diff --git a/css/20_map.css b/css/20_map.css index 4a66b00df..d162eef12 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -1,26 +1,53 @@ - -use { pointer-events: none; } - /* base styles */ .layer-osm path:not(.oneway) { fill: none; } /* IE needs :not(.oneway) */ /* the above fill: none rule affects paths in shadow dom only in Firefox */ .layer-osm use.icon path { fill: #333; } /* FF svg Maki icons */ .layer-osm .turn use path { fill: #000; } /* FF turn restriction icons */ -#turn-only-shape2, #turn-only-u-shape2 { fill: #7092FF; } /* FF turn-only, turn-only-u */ -#turn-no-shape2, #turn-no-u-shape2 { fill: #E06D5F; } /* FF turn-no, turn-no-u */ -#turn-yes-shape2, #turn-yes-u-shape2 { fill: #8CD05F; } /* FF turn-yes, turn-yes-u */ +#turn-only-shape2, #turn-only-u-shape2 { fill: #7092ff; } /* FF turn-only, turn-only-u */ +#turn-no-shape2, #turn-no-u-shape2 { fill: #e06d5f; } /* FF turn-no, turn-no-u */ +#turn-yes-shape2, #turn-yes-u-shape2 { fill: #8cd05f; } /* FF turn-yes, turn-yes-u */ -g.point .shadow, -g.vertex .shadow, -g.midpoint .shadow { - pointer-events: all; + +/* No interactivity except what we specifically allow */ +.layer-osm * { + pointer-events: none; } -path.shadow { +/* `.target` objects are interactive */ +/* They can be picked up, clicked, hovered, or things can connect to them */ +.node.target { + pointer-events: fill; + fill-opacity: 0.8; + fill: currentColor; + stroke: none; +} + +.way.target { pointer-events: stroke; + fill: none; + stroke-width: 12; + stroke-opacity: 0.8; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; } +/* `.target-nope` objects are explicitly forbidden to join to */ +.node.target.target-nope, +.way.target.target-nope { + cursor: not-allowed; +} + + +/* `.active` objects (currently being drawn or dragged) are not interactive */ +/* This is important to allow the events to drop through to whatever is */ +/* below them on the map, so you can still hover and connect to other things. */ +.layer-osm .active { + pointer-events: none !important; +} + + /* points */ g.point .stroke { @@ -45,10 +72,6 @@ g.point.selected .shadow { stroke-opacity: 0.7; } -g.point.active, g.point.active * { - pointer-events: none; -} - g.point ellipse.stroke { display: none; } @@ -88,28 +111,6 @@ g.midpoint .shadow { fill-opacity: 0; } -g.vertex.vertex-hover { - display: none; -} - -.mode-draw-area g.vertex.vertex-hover, -.mode-draw-line g.vertex.vertex-hover, -.mode-add-area g.vertex.vertex-hover, -.mode-add-line g.vertex.vertex-hover, -.mode-add-point g.vertex.vertex-hover, -.mode-drag-node g.vertex.vertex-hover { - display: block; -} - -.mode-draw-area .hover-disabled g.vertex.vertex-hover, -.mode-draw-line .hover-disabled g.vertex.vertex-hover, -.mode-add-area .hover-disabled g.vertex.vertex-hover, -.mode-add-line .hover-disabled g.vertex.vertex-hover, -.mode-add-point .hover-disabled g.vertex.vertex-hover, -.mode-drag-node .hover-disabled g.vertex.vertex-hover { - display: none; -} - g.vertex.related:not(.selected) .shadow, g.vertex.hover:not(.selected) .shadow, g.midpoint.related:not(.selected) .shadow, @@ -121,13 +122,6 @@ g.vertex.selected .shadow { fill-opacity: 0.7; } -.mode-draw-area g.midpoint, -.mode-draw-line g.midpoint, -.mode-add-area g.midpoint, -.mode-add-line g.midpoint, -.mode-add-point g.midpoint { - display: none; -} /* lines */ @@ -138,7 +132,7 @@ g.vertex.selected .shadow { path.line { stroke-linecap: round; - stroke-linejoin: bevel; + stroke-linejoin: round; } path.stroke { @@ -170,8 +164,7 @@ path.line.stroke { /* Labels / Markers */ text { - font-size:10px; - pointer-events: none; + font-size: 10px; color: #222; opacity: 1; } @@ -180,11 +173,11 @@ text { fill: #002F35; } -path.oneway { +.onewaygroup path.oneway, +.viewfieldgroup path.viewfield { stroke-width: 6px; } - text.arealabel-halo, text.linelabel-halo, text.pointlabel-halo, @@ -196,7 +189,6 @@ text.pointlabel { font-size: 12px; font-weight: bold; fill: #333; - pointer-events: none; -webkit-transition: opacity 100ms linear; transition: opacity 100ms linear; -moz-transition: opacity 100ms linear; @@ -210,14 +202,14 @@ text.pointlabel { dominant-baseline: auto; } -.layer-halo text { +.layer-labels-halo text { opacity: 0.7; stroke: #fff; stroke-width: 5px; stroke-miterlimit: 1; } -text.proximate { +text.nolabel { opacity: 0; } @@ -247,8 +239,8 @@ g.turn circle { } .form-field-restrictions .vertex { - pointer-events: none; cursor: auto !important; + pointer-events: none; } .lasso #map { @@ -262,11 +254,11 @@ g.turn circle { } path.gpx { - stroke: #FF26D4; + stroke: #ff26d4; stroke-width: 2; fill: none; } text.gpx { - fill: #FF26D4; + fill: #ff26d4; } diff --git a/css/55_cursors.css b/css/55_cursors.css index db5834e6d..854e2f88b 100644 --- a/css/55_cursors.css +++ b/css/55_cursors.css @@ -1,5 +1,10 @@ /* Cursors */ +.nope, +.nope * { + cursor: not-allowed !important; +} + .map-in-map, #map { cursor: auto; /* Opera */ @@ -49,16 +54,6 @@ cursor: url(img/cursor-select-remove.png), pointer; /* FF */ } -#map .point:active, -#map .vertex:active, -#map .line:active, -#map .area:active, -#map .midpoint:active, -#map .mode-select .selected { - cursor: pointer; /* Opera */ - cursor: url(img/cursor-select-acting.png), pointer; /* FF */ -} - .mode-draw-line #map, .mode-draw-area #map, .mode-add-line #map, diff --git a/css/70_fills.css b/css/70_fills.css index 9402eec5d..1c536510c 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -33,29 +33,9 @@ .fill-partial path.area.fill { fill-opacity: 0; stroke-width: 60px; + pointer-events: none; +} +.mode-browse .fill-partial path.area.fill, +.mode-select .fill-partial path.area.fill { pointer-events: visibleStroke; } - -/* Modes */ - -.mode-draw-line .vertex.active, -.mode-draw-area .vertex.active, -.mode-drag-node .vertex.active { - display: none; -} - -.mode-draw-line .way.active, -.mode-draw-area .way.active, -.mode-drag-node .active { - pointer-events: none; -} - -/* Ensure drawing doesn't interact with area fills. */ -.mode-add-point path.area.fill, -.mode-draw-line path.area.fill, -.mode-draw-area path.area.fill, -.mode-add-line path.area.fill, -.mode-add-area path.area.fill, -.mode-drag-node path.area.fill { - pointer-events: none; -} diff --git a/css/80_app.css b/css/80_app.css index 83205fb0b..bc2f268c0 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2864,6 +2864,7 @@ img.tile-removing { stroke-width: 1; } +.nocolor { color: rgba(0, 0, 0, 0); } .red { color: rgba(255, 0, 0, 0.75); } .green { color: rgba(0, 255, 0, 0.75); } .blue { color: rgba(0, 0, 255, 0.75); } diff --git a/data/intro_graph.json b/data/intro_graph.json index 1647a777e..fcf2b191e 100644 --- a/data/intro_graph.json +++ b/data/intro_graph.json @@ -2984,7 +2984,8 @@ "loc": [-85.643097, 41.942575], "tags": { "highway": "traffic_signals", - "traffic_signals": "signal" + "traffic_signals": "signal", + "traffic_signals:direction": "both" } }, "n1643": { @@ -3840,7 +3841,8 @@ "loc": [-85.63582, 41.942771], "tags": { "highway": "traffic_signals", - "traffic_signals": "emergency" + "traffic_signals": "emergency", + "traffic_signals:direction": "both" } }, "n1835": { @@ -8001,7 +8003,8 @@ "loc": [-85.632793, 41.94405], "tags": { "highway": "traffic_signals", - "traffic_signals": "signal" + "traffic_signals": "signal", + "traffic_signals:direction": "both" } }, "n2749": { @@ -12872,7 +12875,7 @@ }, "n3858": { "id": "n3858", - "loc": [-85.616755, 41.952231] + "loc": [-85.616762, 41.952222] }, "n3859": { "id": "n3859", @@ -12908,11 +12911,11 @@ }, "n3866": { "id": "n3866", - "loc": [-85.616572, 41.951992] + "loc": [-85.616557, 41.951997] }, "n3867": { "id": "n3867", - "loc": [-85.616583, 41.952076] + "loc": [-85.61658, 41.952093] }, "n3868": { "id": "n3868", @@ -12920,7 +12923,7 @@ }, "n3869": { "id": "n3869", - "loc": [-85.616916, 41.952279] + "loc": [-85.616918, 41.952276] }, "n387": { "id": "n387", @@ -12928,7 +12931,7 @@ }, "n3870": { "id": "n3870", - "loc": [-85.617088, 41.952254] + "loc": [-85.617098, 41.952235] }, "n3871": { "id": "n3871", @@ -13284,7 +13287,7 @@ }, "n3950": { "id": "n3950", - "loc": [-85.616494, 41.951959] + "loc": [-85.616502, 41.951946] }, "n3951": { "id": "n3951", @@ -13517,7 +13520,9 @@ "id": "n4", "loc": [-85.622764, 41.950892], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n40": { @@ -15769,7 +15774,8 @@ "loc": [-85.628201, 41.954694], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "forward" } }, "n4501": { @@ -15777,7 +15783,8 @@ "loc": [-85.627921, 41.954783], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "backward" } }, "n4502": { @@ -15785,7 +15792,8 @@ "loc": [-85.62775, 41.954696], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "backward" } }, "n4503": { @@ -15793,35 +15801,44 @@ "loc": [-85.628046, 41.954591], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "forward" } }, "n4504": { "id": "n4504", "loc": [-85.631074, 41.957428], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4505": { "id": "n4505", "loc": [-85.630768, 41.957429], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4506": { "id": "n4506", "loc": [-85.629888, 41.957432], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4507": { "id": "n4507", "loc": [-85.629565, 41.957433], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4508": { @@ -15931,7 +15948,9 @@ "id": "n4528", "loc": [-85.631073, 41.955913], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4529": { @@ -15969,21 +15988,27 @@ "id": "n4535", "loc": [-85.629675, 41.954564], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4536": { "id": "n4536", "loc": [-85.630881, 41.954806], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4537": { "id": "n4537", "loc": [-85.630879, 41.954564], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4538": { @@ -16023,49 +16048,61 @@ "id": "n4543", "loc": [-85.631045, 41.959036], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4544": { "id": "n4544", "loc": [-85.632071, 41.959029], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4545": { "id": "n4545", "loc": [-85.632257, 41.959027], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4546": { "id": "n4546", "loc": [-85.631966, 41.957427], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4547": { "id": "n4547", "loc": [-85.632297, 41.957426], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4548": { "id": "n4548", "loc": [-85.631976, 41.955911], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4549": { "id": "n4549", "loc": [-85.632272, 41.955911], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n455": { @@ -16076,14 +16113,18 @@ "id": "n4550", "loc": [-85.632097, 41.954805], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4551": { "id": "n4551", "loc": [-85.632094, 41.954566], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4552": { @@ -16126,7 +16167,9 @@ "id": "n4560", "loc": [-85.622763, 41.95109], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4561": { @@ -16148,7 +16191,9 @@ "id": "n4564", "loc": [-85.624599, 41.950984], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4565": { @@ -16250,7 +16295,9 @@ "id": "n4583", "loc": [-85.617856, 41.954642], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4584": { @@ -16271,7 +16318,9 @@ "id": "n4586", "loc": [-85.620352, 41.951894], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4587": { @@ -16285,14 +16334,18 @@ "id": "n4588", "loc": [-85.620316, 41.950999], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4589": { "id": "n4589", "loc": [-85.620311, 41.950131], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n459": { @@ -16310,21 +16363,27 @@ "id": "n4591", "loc": [-85.620301, 41.949239], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4592": { "id": "n4592", "loc": [-85.620278, 41.947443], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4593": { "id": "n4593", "loc": [-85.619844, 41.947444], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4594": { @@ -16345,14 +16404,18 @@ "id": "n4596", "loc": [-85.622744, 41.947541], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4597": { "id": "n4597", "loc": [-85.622739, 41.947316], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4598": { @@ -16388,14 +16451,18 @@ "id": "n4601", "loc": [-85.622768, 41.949125], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4602": { "id": "n4602", "loc": [-85.622769, 41.949325], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4603": { @@ -16409,14 +16476,17 @@ "id": "n4604", "loc": [-85.622614, 41.950113], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4605": { "id": "n4605", "loc": [-85.624777, 41.949219], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4606": { @@ -16462,7 +16532,9 @@ "id": "n4611", "loc": [-85.62476, 41.947428], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4612": { @@ -16483,7 +16555,7 @@ }, "n4616": { "id": "n4616", - "loc": [-85.61823, 41.9499] + "loc": [-85.618232, 41.949913] }, "n4617": { "id": "n4617", @@ -16628,7 +16700,9 @@ "id": "n4645", "loc": [-85.635815, 41.942638], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4646": { @@ -16863,14 +16937,18 @@ "id": "n4684", "loc": [-85.635566, 41.940102], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4685": { "id": "n4685", "loc": [-85.635961, 41.940125], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4686": { @@ -16934,14 +17012,18 @@ "id": "n4694", "loc": [-85.637038, 41.942513], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4695": { "id": "n4695", "loc": [-85.637174, 41.941354], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4696": { @@ -16955,14 +17037,16 @@ "id": "n4697", "loc": [-85.638058, 41.941346], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4698": { "id": "n4698", "loc": [-85.638359, 41.941344], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n4699": { @@ -16991,14 +17075,16 @@ "id": "n4701", "loc": [-85.639277, 41.941337], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4702": { "id": "n4702", "loc": [-85.639548, 41.941334], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n4703": { @@ -17016,28 +17102,36 @@ "id": "n4705", "loc": [-85.64049, 41.941327], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4706": { "id": "n4706", "loc": [-85.640803, 41.941324], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4707": { "id": "n4707", "loc": [-85.641717, 41.941317], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "all" } }, "n4708": { "id": "n4708", "loc": [-85.641846, 41.941415], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "all" } }, "n4709": { @@ -17058,21 +17152,27 @@ "id": "n4710", "loc": [-85.642014, 41.941313], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "all" } }, "n4711": { "id": "n4711", "loc": [-85.641854, 41.942455], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4712": { "id": "n4712", "loc": [-85.641859, 41.942739], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4713": { @@ -17086,14 +17186,18 @@ "id": "n4714", "loc": [-85.640669, 41.942716], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4715": { "id": "n4715", "loc": [-85.640664, 41.942478], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4716": { @@ -17107,14 +17211,18 @@ "id": "n4717", "loc": [-85.639455, 41.942731], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4718": { "id": "n4718", "loc": [-85.63945, 41.942492], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4719": { @@ -17132,14 +17240,18 @@ "id": "n4720", "loc": [-85.638238, 41.942745], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4721": { "id": "n4721", "loc": [-85.638233, 41.942511], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4722": { @@ -17167,21 +17279,27 @@ "id": "n4725", "loc": [-85.63704, 41.942741], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4726": { "id": "n4726", "loc": [-85.633467, 41.943818], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4727": { "id": "n4727", "loc": [-85.633987, 41.943531], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4728": { @@ -17288,7 +17406,9 @@ "id": "n4741", "loc": [-85.63481, 41.946056], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4742": { @@ -17313,14 +17433,18 @@ "id": "n4745", "loc": [-85.639487, 41.945042], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4746": { "id": "n4746", "loc": [-85.639635, 41.94387], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4747": { @@ -17334,14 +17458,18 @@ "id": "n4748", "loc": [-85.64055, 41.943862], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4749": { "id": "n4749", "loc": [-85.640864, 41.943859], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n475": { @@ -17352,7 +17480,9 @@ "id": "n4750", "loc": [-85.640718, 41.945022], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4751": { @@ -17366,7 +17496,9 @@ "id": "n4752", "loc": [-85.641913, 41.94502], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4753": { @@ -17380,21 +17512,25 @@ "id": "n4754", "loc": [-85.642045, 41.94385], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n4755": { "id": "n4755", "loc": [-85.641738, 41.943852], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4756": { "id": "n4756", "loc": [-85.642928, 41.943843], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4757": { @@ -17408,14 +17544,18 @@ "id": "n4758", "loc": [-85.642986, 41.945105], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "all" } }, "n4759": { "id": "n4759", "loc": [-85.643136, 41.94502], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "all", + "direction": "forward" } }, "n476": { @@ -27090,7 +27230,25 @@ }, "w660": { "id": "w660", - "nodes": ["n3982", "n3842", "n3864", "n3865", "n3866", "n3867", "n3868", "n3858", "n3869", "n3870", "n3862"], + "nodes": [ + "n3982", + "n3842", + "n3864", + "n3865", + "n2938", + "n3866", + "n2939", + "n3867", + "n3868", + "n3858", + "n2937", + "n3869", + "n2935", + "n2934", + "n3870", + "n3348", + "n3862" + ], "tags": { "highway": "service" } @@ -27299,8 +27457,12 @@ "n4002", "n4003", "n3949", + "n3351", "n3950", + "n3354", + "n3350", "n3951", + "n3349", "n3952", "n3953", "n3954", @@ -29256,6 +29418,46 @@ "tags": { "amenity": "parking" } + }, + "n2934": { + "id": "n2934", + "loc": [-85.617051, 41.952263] + }, + "n2935": { + "id": "n2935", + "loc": [-85.61699, 41.952276] + }, + "n2937": { + "id": "n2937", + "loc": [-85.616847, 41.952262] + }, + "n2938": { + "id": "n2938", + "loc": [-85.616577, 41.951956] + }, + "n2939": { + "id": "n2939", + "loc": [-85.61656, 41.952044] + }, + "n3348": { + "id": "n3348", + "loc": [-85.61714, 41.9522] + }, + "n3349": { + "id": "n3349", + "loc": [-85.616517, 41.95212] + }, + "n3350": { + "id": "n3350", + "loc": [-85.616489, 41.952033] + }, + "n3351": { + "id": "n3351", + "loc": [-85.616529, 41.951907] + }, + "n3354": { + "id": "n3354", + "loc": [-85.616488, 41.951994] } } } diff --git a/data/presets.yaml b/data/presets.yaml index 774db7700..5ed920b01 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -295,53 +295,9 @@ en: label: Capacity # capacity field placeholder placeholder: '50, 100, 200...' - cardinal_direction: - # direction=* - label: Direction - options: - # direction=E - E: East - # direction=ENE - ENE: East-northeast - # direction=ESE - ESE: East-southeast - # direction=N - 'N': North - # direction=NE - NE: Northeast - # direction=NNE - NNE: North-northeast - # direction=NNW - NNW: North-northwest - # direction=NW - NW: Northwest - # direction=S - S: South - # direction=SE - SE: Southeast - # direction=SSE - SSE: South-southeast - # direction=SSW - SSW: South-southwest - # direction=SW - SW: Southwest - # direction=W - W: West - # direction=WNW - WNW: West-northwest - # direction=WSW - WSW: West-southwest castle_type: # castle_type=* label: Type - clock_direction: - # direction=* - label: Direction - options: - # direction=anticlockwise - anticlockwise: Counterclockwise - # direction=clockwise - clockwise: Clockwise clothes: # clothes=* label: Clothes @@ -469,6 +425,65 @@ en: diaper: # diaper=* label: Diaper Changing Available + direction: + # direction=* + label: Direction (Degrees Clockwise) + # direction field placeholder + placeholder: '45, 90, 180, 270' + direction_cardinal: + # direction=* + label: Direction + options: + # direction=E + E: East + # direction=ENE + ENE: East-northeast + # direction=ESE + ESE: East-southeast + # direction=N + 'N': North + # direction=NE + NE: Northeast + # direction=NNE + NNE: North-northeast + # direction=NNW + NNW: North-northwest + # direction=NW + NW: Northwest + # direction=S + S: South + # direction=SE + SE: Southeast + # direction=SSE + SSE: South-southeast + # direction=SSW + SSW: South-southwest + # direction=SW + SW: Southwest + # direction=W + W: West + # direction=WNW + WNW: West-northwest + # direction=WSW + WSW: West-southwest + direction_clock: + # direction=* + label: Direction + options: + # direction=anticlockwise + anticlockwise: Counterclockwise + # direction=clockwise + clockwise: Clockwise + direction_vertex: + # direction=* + label: Direction + options: + # direction=backward + backward: Backward + # direction=both + both: Both / All + # direction=forward + forward: Forward display: # display=* label: Display @@ -815,11 +830,6 @@ en: memorial: # memorial=* label: Type - milestone_position: - # 'railway:position=*' - label: Milestone Position - # milestone_position field placeholder - placeholder: Distance to one decimal (123.4) monitoring_multi: # 'monitoring:=*' label: Monitoring @@ -973,14 +983,6 @@ en: label: Par # par field placeholder placeholder: '3, 4, 5...' - parallel_direction: - # direction=* - label: Direction - options: - # direction=backward - backward: Backward - # direction=forward - forward: Forward park_ride: # park_ride=* label: Park and Ride @@ -1111,6 +1113,21 @@ en: railway: # railway=* label: Type + railway/position: + # 'railway:position=*' + label: Milestone Position + # railway/position field placeholder + placeholder: Distance to one decimal (123.4) + railway/signal/direction: + # 'railway:signal:direction=*' + label: Direction + options: + # 'railway:signal:direction=backward' + backward: Backward + # 'railway:signal:direction=both' + both: Both / All + # 'railway:signal:direction=forward' + forward: Forward rating: # rating=* label: Power Rating @@ -1465,6 +1482,16 @@ en: traffic_signals: # traffic_signals=* label: Type + traffic_signals/direction: + # 'traffic_signals:direction=*' + label: Direction + options: + # 'traffic_signals:direction=backward' + backward: Backward + # 'traffic_signals:direction=both' + both: Both / All + # 'traffic_signals:direction=forward' + forward: Forward trail_visibility: # trail_visibility=* label: Trail Visibility diff --git a/data/presets/fields.json b/data/presets/fields.json index 803f6ca39..17def7fd9 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -394,47 +394,11 @@ "label": "Capacity", "placeholder": "50, 100, 200..." }, - "cardinal_direction": { - "key": "direction", - "type": "combo", - "label": "Direction", - "strings": { - "options": { - "N": "North", - "E": "East", - "S": "South", - "W": "West", - "NE": "Northeast", - "SE": "Southeast", - "SW": "Southwest", - "NW": "Northwest", - "NNE": "North-northeast", - "ENE": "East-northeast", - "ESE": "East-southeast", - "SSE": "South-southeast", - "SSW": "South-southwest", - "WSW": "West-southwest", - "WNW": "West-northwest", - "NNW": "North-northwest" - } - } - }, "castle_type": { "key": "castle_type", "type": "combo", "label": "Type" }, - "clock_direction": { - "key": "direction", - "type": "combo", - "label": "Direction", - "strings": { - "options": { - "clockwise": "Clockwise", - "anticlockwise": "Counterclockwise" - } - } - }, "clothes": { "key": "clothes", "type": "semiCombo", @@ -631,6 +595,60 @@ "5" ] }, + "direction_cardinal": { + "key": "direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "N": "North", + "E": "East", + "S": "South", + "W": "West", + "NE": "Northeast", + "SE": "Southeast", + "SW": "Southwest", + "NW": "Northwest", + "NNE": "North-northeast", + "ENE": "East-northeast", + "ESE": "East-southeast", + "SSE": "South-southeast", + "SSW": "South-southwest", + "WSW": "West-southwest", + "WNW": "West-northwest", + "NNW": "North-northwest" + } + } + }, + "direction_clock": { + "key": "direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "clockwise": "Clockwise", + "anticlockwise": "Counterclockwise" + } + } + }, + "direction_vertex": { + "key": "direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } + }, + "direction": { + "key": "direction", + "type": "number", + "label": "Direction (Degrees Clockwise)", + "placeholder": "45, 90, 180, 270" + }, "display": { "key": "display", "type": "combo", @@ -1140,12 +1158,6 @@ "type": "typeCombo", "label": "Type" }, - "milestone_position": { - "key": "railway:position", - "type": "text", - "placeholder": "Distance to one decimal (123.4)", - "label": "Milestone Position" - }, "monitoring_multi": { "key": "monitoring:", "type": "multiCombo", @@ -1321,17 +1333,6 @@ "label": "Par", "placeholder": "3, 4, 5..." }, - "parallel_direction": { - "key": "direction", - "type": "combo", - "label": "Direction", - "strings": { - "options": { - "forward": "Forward", - "backward": "Backward" - } - } - }, "park_ride": { "key": "park_ride", "type": "check", @@ -1483,6 +1484,24 @@ "type": "typeCombo", "label": "Type" }, + "railway/position": { + "key": "railway:position", + "type": "text", + "placeholder": "Distance to one decimal (123.4)", + "label": "Milestone Position" + }, + "railway/signal/direction": { + "key": "railway:signal:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } + }, "rating": { "key": "rating", "type": "combo", @@ -2005,6 +2024,18 @@ "label": "Type", "default": "signal" }, + "traffic_signals/direction": { + "key": "traffic_signals:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } + }, "trail_visibility": { "key": "trail_visibility", "type": "combo", diff --git a/data/presets/fields/direction.json b/data/presets/fields/direction.json new file mode 100644 index 000000000..c4324ce8e --- /dev/null +++ b/data/presets/fields/direction.json @@ -0,0 +1,6 @@ +{ + "key": "direction", + "type": "number", + "label": "Direction (Degrees Clockwise)", + "placeholder": "45, 90, 180, 270" +} diff --git a/data/presets/fields/cardinal_direction.json b/data/presets/fields/direction_cardinal.json similarity index 100% rename from data/presets/fields/cardinal_direction.json rename to data/presets/fields/direction_cardinal.json diff --git a/data/presets/fields/clock_direction.json b/data/presets/fields/direction_clock.json similarity index 100% rename from data/presets/fields/clock_direction.json rename to data/presets/fields/direction_clock.json diff --git a/data/presets/fields/parallel_direction.json b/data/presets/fields/direction_vertex.json similarity index 71% rename from data/presets/fields/parallel_direction.json rename to data/presets/fields/direction_vertex.json index 03801f0f3..9b0d7ebc0 100644 --- a/data/presets/fields/parallel_direction.json +++ b/data/presets/fields/direction_vertex.json @@ -5,7 +5,8 @@ "strings": { "options": { "forward": "Forward", - "backward": "Backward" + "backward": "Backward", + "both": "Both / All" } } } diff --git a/data/presets/fields/milestone_position.json b/data/presets/fields/railway/position.json similarity index 100% rename from data/presets/fields/milestone_position.json rename to data/presets/fields/railway/position.json diff --git a/data/presets/fields/railway/signal/direction.json b/data/presets/fields/railway/signal/direction.json new file mode 100644 index 000000000..3034345b8 --- /dev/null +++ b/data/presets/fields/railway/signal/direction.json @@ -0,0 +1,12 @@ +{ + "key": "railway:signal:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } +} diff --git a/data/presets/fields/traffic_signals/direction.json b/data/presets/fields/traffic_signals/direction.json new file mode 100644 index 000000000..079c2a133 --- /dev/null +++ b/data/presets/fields/traffic_signals/direction.json @@ -0,0 +1,12 @@ +{ + "key": "traffic_signals:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } +} diff --git a/data/presets/presets.json b/data/presets/presets.json index 139bfbd16..9489fab67 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -170,7 +170,7 @@ }, "advertising/billboard": { "fields": [ - "parallel_direction", + "direction", "lit" ], "geometry": [ @@ -6899,7 +6899,7 @@ "highway/give_way": { "icon": "poi-yield", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" @@ -6944,7 +6944,7 @@ "highway": "mini_roundabout" }, "fields": [ - "clock_direction" + "direction_clock" ], "name": "Mini-Roundabout" }, @@ -7477,6 +7477,7 @@ "vertex" ], "fields": [ + "direction", "ref" ], "tags": { @@ -7511,7 +7512,7 @@ "icon": "poi-stop", "fields": [ "stop", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" @@ -7537,6 +7538,7 @@ }, "fields": [ "lamp_type", + "direction", "ref" ], "terms": [ @@ -7648,6 +7650,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "highway": "traffic_mirror" }, @@ -7673,7 +7678,8 @@ "highway": "traffic_signals" }, "fields": [ - "traffic_signals" + "traffic_signals", + "traffic_signals/direction" ], "terms": [ "light", @@ -10471,7 +10477,8 @@ "area" ], "fields": [ - "operator" + "operator", + "direction" ], "terms": [ "entrance", @@ -10804,7 +10811,8 @@ "fields": [ "surveillance", "surveillance/type", - "surveillance/zone" + "surveillance/zone", + "direction" ], "terms": [ "anpr", @@ -11131,7 +11139,8 @@ ], "fields": [ "fee", - "access_simple" + "access_simple", + "direction" ], "tags": { "natural": "cave_entrance" @@ -15041,7 +15050,7 @@ "vertex" ], "fields": [ - "milestone_position" + "railway/position" ], "tags": { "railway": "milestone" @@ -15137,6 +15146,11 @@ "point", "vertex" ], + "fields": [ + "railway/position", + "railway/signal/direction", + "ref" + ], "tags": { "railway": "signal" }, @@ -18137,7 +18151,8 @@ "fields": [ "name", "operator", - "board_type" + "board_type", + "direction" ], "geometry": [ "point", @@ -18181,7 +18196,8 @@ "fields": [ "operator", "map_type", - "map_size" + "map_size", + "direction" ], "geometry": [ "point", @@ -18315,6 +18331,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "tourism": "viewpoint" }, @@ -18366,7 +18385,7 @@ "icon": "poi-warning", "fields": [ "traffic_calming", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18387,7 +18406,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18406,7 +18425,7 @@ "traffic_calming/chicane": { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18425,7 +18444,7 @@ "traffic_calming/choker": { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18444,7 +18463,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18465,7 +18484,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18484,7 +18503,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18518,7 +18537,7 @@ "traffic_calming/rumble_strip": { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/advertising/billboard.json b/data/presets/presets/advertising/billboard.json index e3cc660ba..1259e8888 100644 --- a/data/presets/presets/advertising/billboard.json +++ b/data/presets/presets/advertising/billboard.json @@ -1,6 +1,6 @@ { "fields": [ - "parallel_direction", + "direction", "lit" ], "geometry": [ diff --git a/data/presets/presets/highway/give_way.json b/data/presets/presets/highway/give_way.json index 232c27d25..3477820fc 100644 --- a/data/presets/presets/highway/give_way.json +++ b/data/presets/presets/highway/give_way.json @@ -1,7 +1,7 @@ { "icon": "poi-yield", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" diff --git a/data/presets/presets/highway/mini_roundabout.json b/data/presets/presets/highway/mini_roundabout.json index f6ec98f34..b290cbb18 100644 --- a/data/presets/presets/highway/mini_roundabout.json +++ b/data/presets/presets/highway/mini_roundabout.json @@ -7,7 +7,7 @@ "highway": "mini_roundabout" }, "fields": [ - "clock_direction" + "direction_clock" ], "name": "Mini-Roundabout" } diff --git a/data/presets/presets/highway/speed_camera.json b/data/presets/presets/highway/speed_camera.json index f0bea39da..c3f9afc32 100644 --- a/data/presets/presets/highway/speed_camera.json +++ b/data/presets/presets/highway/speed_camera.json @@ -5,6 +5,7 @@ "vertex" ], "fields": [ + "direction", "ref" ], "tags": { diff --git a/data/presets/presets/highway/stop.json b/data/presets/presets/highway/stop.json index 47b5e35ed..ec4576d12 100644 --- a/data/presets/presets/highway/stop.json +++ b/data/presets/presets/highway/stop.json @@ -2,7 +2,7 @@ "icon": "poi-stop", "fields": [ "stop", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" diff --git a/data/presets/presets/highway/street_lamp.json b/data/presets/presets/highway/street_lamp.json index d72d74bc8..8c48d005f 100644 --- a/data/presets/presets/highway/street_lamp.json +++ b/data/presets/presets/highway/street_lamp.json @@ -9,6 +9,7 @@ }, "fields": [ "lamp_type", + "direction", "ref" ], "terms": [ diff --git a/data/presets/presets/highway/traffic_mirror.json b/data/presets/presets/highway/traffic_mirror.json index 29ca4d78f..f2d3f066d 100644 --- a/data/presets/presets/highway/traffic_mirror.json +++ b/data/presets/presets/highway/traffic_mirror.json @@ -3,6 +3,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "highway": "traffic_mirror" }, diff --git a/data/presets/presets/highway/traffic_signals.json b/data/presets/presets/highway/traffic_signals.json index 3dee65640..2350052fd 100644 --- a/data/presets/presets/highway/traffic_signals.json +++ b/data/presets/presets/highway/traffic_signals.json @@ -7,7 +7,8 @@ "highway": "traffic_signals" }, "fields": [ - "traffic_signals" + "traffic_signals", + "traffic_signals/direction" ], "terms": [ "light", diff --git a/data/presets/presets/man_made/adit.json b/data/presets/presets/man_made/adit.json index fdc535208..b71decf24 100644 --- a/data/presets/presets/man_made/adit.json +++ b/data/presets/presets/man_made/adit.json @@ -5,7 +5,8 @@ "area" ], "fields": [ - "operator" + "operator", + "direction" ], "terms": [ "entrance", diff --git a/data/presets/presets/man_made/surveillance.json b/data/presets/presets/man_made/surveillance.json index 98ec69379..2bb824dc4 100644 --- a/data/presets/presets/man_made/surveillance.json +++ b/data/presets/presets/man_made/surveillance.json @@ -7,7 +7,8 @@ "fields": [ "surveillance", "surveillance/type", - "surveillance/zone" + "surveillance/zone", + "direction" ], "terms": [ "anpr", diff --git a/data/presets/presets/natural/cave_entrance.json b/data/presets/presets/natural/cave_entrance.json index 6dba4ed02..b8fa4023a 100644 --- a/data/presets/presets/natural/cave_entrance.json +++ b/data/presets/presets/natural/cave_entrance.json @@ -6,7 +6,8 @@ ], "fields": [ "fee", - "access_simple" + "access_simple", + "direction" ], "tags": { "natural": "cave_entrance" diff --git a/data/presets/presets/railway/milestone.json b/data/presets/presets/railway/milestone.json index f3bd6de5e..328eef562 100644 --- a/data/presets/presets/railway/milestone.json +++ b/data/presets/presets/railway/milestone.json @@ -5,7 +5,7 @@ "vertex" ], "fields": [ - "milestone_position" + "railway/position" ], "tags": { "railway": "milestone" diff --git a/data/presets/presets/railway/signal.json b/data/presets/presets/railway/signal.json index d4fe389fd..9c62f5276 100644 --- a/data/presets/presets/railway/signal.json +++ b/data/presets/presets/railway/signal.json @@ -4,6 +4,11 @@ "point", "vertex" ], + "fields": [ + "railway/position", + "railway/signal/direction", + "ref" + ], "tags": { "railway": "signal" }, diff --git a/data/presets/presets/tourism/information/board.json b/data/presets/presets/tourism/information/board.json index 40b2e504c..0acc5a545 100644 --- a/data/presets/presets/tourism/information/board.json +++ b/data/presets/presets/tourism/information/board.json @@ -3,7 +3,8 @@ "fields": [ "name", "operator", - "board_type" + "board_type", + "direction" ], "geometry": [ "point", diff --git a/data/presets/presets/tourism/information/map.json b/data/presets/presets/tourism/information/map.json index 0527009f9..fce5d3418 100644 --- a/data/presets/presets/tourism/information/map.json +++ b/data/presets/presets/tourism/information/map.json @@ -3,7 +3,8 @@ "fields": [ "operator", "map_type", - "map_size" + "map_size", + "direction" ], "geometry": [ "point", diff --git a/data/presets/presets/tourism/viewpoint.json b/data/presets/presets/tourism/viewpoint.json index 8f6fc1baa..1d7ebb4eb 100644 --- a/data/presets/presets/tourism/viewpoint.json +++ b/data/presets/presets/tourism/viewpoint.json @@ -4,6 +4,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "tourism": "viewpoint" }, diff --git a/data/presets/presets/traffic_calming.json b/data/presets/presets/traffic_calming.json index 0cb5d8fca..5048a9e73 100644 --- a/data/presets/presets/traffic_calming.json +++ b/data/presets/presets/traffic_calming.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "traffic_calming", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/bump.json b/data/presets/presets/traffic_calming/bump.json index 071ed8e2c..2d281beab 100644 --- a/data/presets/presets/traffic_calming/bump.json +++ b/data/presets/presets/traffic_calming/bump.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/chicane.json b/data/presets/presets/traffic_calming/chicane.json index bbdbfa068..3fbd53cb0 100644 --- a/data/presets/presets/traffic_calming/chicane.json +++ b/data/presets/presets/traffic_calming/chicane.json @@ -1,7 +1,7 @@ { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/choker.json b/data/presets/presets/traffic_calming/choker.json index bee59649a..78688ef90 100644 --- a/data/presets/presets/traffic_calming/choker.json +++ b/data/presets/presets/traffic_calming/choker.json @@ -1,7 +1,7 @@ { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/cushion.json b/data/presets/presets/traffic_calming/cushion.json index 788b2c4ba..47a3d5930 100644 --- a/data/presets/presets/traffic_calming/cushion.json +++ b/data/presets/presets/traffic_calming/cushion.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/dip.json b/data/presets/presets/traffic_calming/dip.json index 6098025ba..aecc88ab4 100644 --- a/data/presets/presets/traffic_calming/dip.json +++ b/data/presets/presets/traffic_calming/dip.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/hump.json b/data/presets/presets/traffic_calming/hump.json index fd724701f..3c17b96be 100644 --- a/data/presets/presets/traffic_calming/hump.json +++ b/data/presets/presets/traffic_calming/hump.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/rumble_strip.json b/data/presets/presets/traffic_calming/rumble_strip.json index f706db468..37a3ca57f 100644 --- a/data/presets/presets/traffic_calming/rumble_strip.json +++ b/data/presets/presets/traffic_calming/rumble_strip.json @@ -1,7 +1,7 @@ { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/dist/locales/en.json b/dist/locales/en.json index dea3729a3..283774bf3 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -1501,37 +1501,9 @@ "label": "Capacity", "placeholder": "50, 100, 200..." }, - "cardinal_direction": { - "label": "Direction", - "options": { - "N": "North", - "E": "East", - "S": "South", - "W": "West", - "NE": "Northeast", - "SE": "Southeast", - "SW": "Southwest", - "NW": "Northwest", - "NNE": "North-northeast", - "ENE": "East-northeast", - "ESE": "East-southeast", - "SSE": "South-southeast", - "SSW": "South-southwest", - "WSW": "West-southwest", - "WNW": "West-northwest", - "NNW": "North-northwest" - } - }, "castle_type": { "label": "Type" }, - "clock_direction": { - "label": "Direction", - "options": { - "clockwise": "Clockwise", - "anticlockwise": "Counterclockwise" - } - }, "clothes": { "label": "Clothes" }, @@ -1654,6 +1626,46 @@ "diaper": { "label": "Diaper Changing Available" }, + "direction_cardinal": { + "label": "Direction", + "options": { + "N": "North", + "E": "East", + "S": "South", + "W": "West", + "NE": "Northeast", + "SE": "Southeast", + "SW": "Southwest", + "NW": "Northwest", + "NNE": "North-northeast", + "ENE": "East-northeast", + "ESE": "East-southeast", + "SSE": "South-southeast", + "SSW": "South-southwest", + "WSW": "West-southwest", + "WNW": "West-northwest", + "NNW": "North-northwest" + } + }, + "direction_clock": { + "label": "Direction", + "options": { + "clockwise": "Clockwise", + "anticlockwise": "Counterclockwise" + } + }, + "direction_vertex": { + "label": "Direction", + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + }, + "direction": { + "label": "Direction (Degrees Clockwise)", + "placeholder": "45, 90, 180, 270" + }, "display": { "label": "Display" }, @@ -1956,10 +1968,6 @@ "memorial": { "label": "Type" }, - "milestone_position": { - "label": "Milestone Position", - "placeholder": "Distance to one decimal (123.4)" - }, "monitoring_multi": { "label": "Monitoring" }, @@ -2077,13 +2085,6 @@ "label": "Par", "placeholder": "3, 4, 5..." }, - "parallel_direction": { - "label": "Direction", - "options": { - "forward": "Forward", - "backward": "Backward" - } - }, "park_ride": { "label": "Park and Ride" }, @@ -2185,6 +2186,18 @@ "railway": { "label": "Type" }, + "railway/position": { + "label": "Milestone Position", + "placeholder": "Distance to one decimal (123.4)" + }, + "railway/signal/direction": { + "label": "Direction", + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + }, "rating": { "label": "Power Rating" }, @@ -2487,6 +2500,14 @@ "traffic_signals": { "label": "Type" }, + "traffic_signals/direction": { + "label": "Direction", + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + }, "trail_visibility": { "label": "Trail Visibility", "placeholder": "Excellent, Good, Bad...", diff --git a/modules/actions/circularize.js b/modules/actions/circularize.js index dc66825ac..908edb349 100644 --- a/modules/actions/circularize.js +++ b/modules/actions/circularize.js @@ -10,11 +10,7 @@ import { polygonCentroid as d3_polygonCentroid } from 'd3-polygon'; -import { - geoEuclideanDistance, - geoInterp -} from '../geo'; - +import { geoVecInterp, geoVecLength } from '../geo'; import { osmNode } from '../osm'; @@ -41,8 +37,8 @@ export function actionCircularize(wayId, projection, maxAngle) { keyNodes = nodes.filter(function(n) { return graph.parentWays(n).length !== 1; }), points = nodes.map(function(n) { return projection(n.loc); }), keyPoints = keyNodes.map(function(n) { return projection(n.loc); }), - centroid = (points.length === 2) ? geoInterp(points[0], points[1], 0.5) : d3_polygonCentroid(points), - radius = d3_median(points, function(p) { return geoEuclideanDistance(centroid, p); }), + centroid = (points.length === 2) ? geoVecInterp(points[0], points[1], 0.5) : d3_polygonCentroid(points), + radius = d3_median(points, function(p) { return geoVecLength(centroid, p); }), sign = d3_polygonArea(points) > 0 ? 1 : -1, ids; @@ -82,7 +78,7 @@ export function actionCircularize(wayId, projection, maxAngle) { } // position this key node - var distance = geoEuclideanDistance(centroid, keyPoints[i]); + var distance = geoVecLength(centroid, keyPoints[i]); if (distance === 0) { distance = 1e-4; } keyPoints[i] = [ centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius, @@ -91,7 +87,7 @@ export function actionCircularize(wayId, projection, maxAngle) { loc = projection.invert(keyPoints[i]); node = keyNodes[i]; origNode = origNodes[node.id]; - node = node.move(geoInterp(origNode.loc, loc, t)); + node = node.move(geoVecInterp(origNode.loc, loc, t)); graph = graph.replace(node); // figure out the between delta angle we want to match to @@ -122,7 +118,7 @@ export function actionCircularize(wayId, projection, maxAngle) { origNode = origNodes[node.id]; nearNodes[node.id] = angle; - node = node.move(geoInterp(origNode.loc, loc, t)); + node = node.move(geoVecInterp(origNode.loc, loc, t)); graph = graph.replace(node); } @@ -145,7 +141,7 @@ export function actionCircularize(wayId, projection, maxAngle) { } } - node = osmNode({ loc: geoInterp(origNode.loc, loc, t) }); + node = osmNode({ loc: geoVecInterp(origNode.loc, loc, t) }); graph = graph.replace(node); nodes.splice(endNodeIndex + j, 0, node); @@ -220,7 +216,7 @@ export function actionCircularize(wayId, projection, maxAngle) { // move interior nodes to the surface of the convex hull.. for (var j = 1; j < indexRange; j++) { - var point = geoInterp(hull[i], hull[i+1], j / indexRange), + var point = geoVecInterp(hull[i], hull[i+1], j / indexRange), node = nodes[(j + startIndex) % nodes.length].move(projection.invert(point)); graph = graph.replace(node); } diff --git a/modules/actions/move.js b/modules/actions/move.js index ddc1da8a8..1f400085c 100644 --- a/modules/actions/move.js +++ b/modules/actions/move.js @@ -11,22 +11,21 @@ import _without from 'lodash-es/without'; import { osmNode } from '../osm'; import { - geoChooseEdge, geoAngle, - geoInterp, + geoChooseEdge, geoPathIntersections, geoPathLength, - geoSphericalDistance + geoSphericalDistance, + geoVecAdd, + geoVecInterp, + geoVecSubtract } from '../geo'; // https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as export function actionMove(moveIds, tryDelta, projection, cache) { - var delta = tryDelta; - - function vecAdd(a, b) { return [a[0] + b[0], a[1] + b[1]]; } - function vecSub(a, b) { return [a[0] - b[0], a[1] - b[1]]; } + var _delta = tryDelta; function setupCache(graph) { function canMove(nodeId) { @@ -118,11 +117,11 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Place a vertex where the moved vertex used to be, to preserve way shape.. - function replaceMovedVertex(nodeId, wayId, graph, delta) { - var way = graph.entity(wayId), - moved = graph.entity(nodeId), - movedIndex = way.nodes.indexOf(nodeId), - len, prevIndex, nextIndex; + function replaceMovedVertex(nodeId, wayId, graph, _delta) { + var way = graph.entity(wayId); + var moved = graph.entity(nodeId); + var movedIndex = way.nodes.indexOf(nodeId); + var len, prevIndex, nextIndex; if (way.isClosed()) { len = way.nodes.length - 1; @@ -134,14 +133,14 @@ export function actionMove(moveIds, tryDelta, projection, cache) { nextIndex = movedIndex + 1; } - var prev = graph.hasEntity(way.nodes[prevIndex]), - next = graph.hasEntity(way.nodes[nextIndex]); + var prev = graph.hasEntity(way.nodes[prevIndex]); + var next = graph.hasEntity(way.nodes[nextIndex]); // Don't add orig vertex at endpoint.. if (!prev || !next) return graph; - var key = wayId + '_' + nodeId, - orig = cache.replacedVertex[key]; + var key = wayId + '_' + nodeId; + var orig = cache.replacedVertex[key]; if (!orig) { orig = osmNode(); cache.replacedVertex[key] = orig; @@ -149,9 +148,9 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } var start, end; - if (delta) { + if (_delta) { start = projection(cache.startLoc[nodeId]); - end = projection.invert(vecAdd(start, delta)); + end = projection.invert(geoVecAdd(start, _delta)); } else { end = cache.startLoc[nodeId]; } @@ -184,30 +183,30 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Reorder nodes around intersections that have moved.. function unZorroIntersection(intersection, graph) { - var vertex = graph.entity(intersection.nodeId), - way1 = graph.entity(intersection.movedId), - way2 = graph.entity(intersection.unmovedId), - isEP1 = intersection.movedIsEP, - isEP2 = intersection.unmovedIsEP; + var vertex = graph.entity(intersection.nodeId); + var way1 = graph.entity(intersection.movedId); + var way2 = graph.entity(intersection.unmovedId); + var isEP1 = intersection.movedIsEP; + var isEP2 = intersection.unmovedIsEP; // don't move the vertex if it is the endpoint of both ways. if (isEP1 && isEP2) return graph; - var nodes1 = _without(graph.childNodes(way1), vertex), - nodes2 = _without(graph.childNodes(way2), vertex); + var nodes1 = _without(graph.childNodes(way1), vertex); + var nodes2 = _without(graph.childNodes(way2), vertex); if (way1.isClosed() && way1.first() === vertex.id) nodes1.push(nodes1[0]); if (way2.isClosed() && way2.first() === vertex.id) nodes2.push(nodes2[0]); - var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection), - edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection), - loc; + var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection); + var edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection); + var loc; // snap vertex to nearest edge (or some point between them).. if (!isEP1 && !isEP2) { var epsilon = 1e-4, maxIter = 10; for (var i = 0; i < maxIter; i++) { - loc = geoInterp(edge1.loc, edge2.loc, 0.5); + loc = geoVecInterp(edge1.loc, edge2.loc, 0.5); edge1 = geoChooseEdge(nodes1, projection(loc), projection); edge2 = geoChooseEdge(nodes2, projection(loc), projection); if (Math.abs(edge1.distance - edge2.distance) < epsilon) break; @@ -236,7 +235,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { function cleanupIntersections(graph) { _each(cache.intersection, function(obj) { - graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, delta); + graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, _delta); graph = replaceMovedVertex(obj.nodeId, obj.unmovedId, graph, null); graph = unZorroIntersection(obj, graph); }); @@ -245,7 +244,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } - // check if moving way endpoint can cross an unmoved way, if so limit delta.. + // check if moving way endpoint can cross an unmoved way, if so limit _delta.. function limitDelta(graph) { _each(cache.intersection, function(obj) { // Don't limit movement if this is vertex joins 2 endpoints.. @@ -253,27 +252,28 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Don't limit movement if this vertex is not an endpoint anyway.. if (!obj.movedIsEP) return; - var node = graph.entity(obj.nodeId), - start = projection(node.loc), - end = vecAdd(start, delta), - movedNodes = graph.childNodes(graph.entity(obj.movedId)), - movedPath = _map(_map(movedNodes, 'loc'), - function(loc) { return vecAdd(projection(loc), delta); }), - unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)), - unmovedPath = _map(_map(unmovedNodes, 'loc'), projection), - hits = geoPathIntersections(movedPath, unmovedPath); + var node = graph.entity(obj.nodeId); + var start = projection(node.loc); + var end = geoVecAdd(start, _delta); + var movedNodes = graph.childNodes(graph.entity(obj.movedId)); + var movedPath = _map(_map(movedNodes, 'loc'), function(loc) { + return geoVecAdd(projection(loc), _delta); + }); + var unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)); + var unmovedPath = _map(_map(unmovedNodes, 'loc'), projection); + var hits = geoPathIntersections(movedPath, unmovedPath); for (var i = 0; i < hits.length; i++) { if (_isEqual(hits[i], end)) continue; var edge = geoChooseEdge(unmovedNodes, end, projection); - delta = vecSub(projection(edge.loc), start); + _delta = geoVecSubtract(projection(edge.loc), start); } }); } var action = function(graph) { - if (delta[0] === 0 && delta[1] === 0) return graph; + if (_delta[0] === 0 && _delta[1] === 0) return graph; setupCache(graph); @@ -282,9 +282,9 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } _each(cache.nodes, function(id) { - var node = graph.entity(id), - start = projection(node.loc), - end = vecAdd(start, delta); + var node = graph.entity(id); + var start = projection(node.loc); + var end = geoVecAdd(start, _delta); graph = graph.replace(node.move(projection.invert(end))); }); @@ -297,7 +297,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { action.delta = function() { - return delta; + return _delta; }; diff --git a/modules/actions/move_node.js b/modules/actions/move_node.js index 6e2593b45..4288dcbc5 100644 --- a/modules/actions/move_node.js +++ b/modules/actions/move_node.js @@ -1,7 +1,18 @@ -// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java -// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as -export function actionMoveNode(nodeId, loc) { - return function(graph) { - return graph.replace(graph.entity(nodeId).move(loc)); +import { geoVecInterp } from '../geo'; + +export function actionMoveNode(nodeID, toLoc) { + + var action = function(graph, t) { + if (t === null || !isFinite(t)) t = 1; + t = Math.min(Math.max(+t, 0), 1); + + var node = graph.entity(nodeID); + return graph.replace( + node.move(geoVecInterp(node.loc, toLoc, t)) + ); }; + + action.transitionable = true; + + return action; } diff --git a/modules/actions/orthogonalize.js b/modules/actions/orthogonalize.js index 33b4a4c61..b9d64b205 100644 --- a/modules/actions/orthogonalize.js +++ b/modules/actions/orthogonalize.js @@ -2,10 +2,7 @@ import _clone from 'lodash-es/clone'; import _uniq from 'lodash-es/uniq'; import { actionDeleteNode } from './delete_node'; -import { - geoEuclideanDistance, - geoInterp -} from '../geo'; +import { geoVecInterp, geoVecLength } from '../geo'; /* @@ -40,7 +37,7 @@ export function actionOrthogonalize(wayId, projection) { node = graph.entity(nodes[corner.i].id); loc = projection.invert(points[corner.i]); - graph = graph.replace(node.move(geoInterp(node.loc, loc, t))); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t))); } else { var best, @@ -69,7 +66,7 @@ export function actionOrthogonalize(wayId, projection) { if (originalPoints[i][0] !== points[i][0] || originalPoints[i][1] !== points[i][1]) { loc = projection.invert(points[i]); node = graph.entity(nodes[i].id); - graph = graph.replace(node.move(geoInterp(node.loc, loc, t))); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t))); } } @@ -100,7 +97,7 @@ export function actionOrthogonalize(wayId, projection) { q = subtractPoints(c, b), scale, dotp; - scale = 2 * Math.min(geoEuclideanDistance(p, [0, 0]), geoEuclideanDistance(q, [0, 0])); + scale = 2 * Math.min(geoVecLength(p, [0, 0]), geoVecLength(q, [0, 0])); p = normalizePoint(p, 1.0); q = normalizePoint(q, 1.0); diff --git a/modules/actions/reflect.js b/modules/actions/reflect.js index b27c9eadf..be435c5f9 100644 --- a/modules/actions/reflect.js +++ b/modules/actions/reflect.js @@ -4,10 +4,10 @@ import { } from 'd3-polygon'; import { - geoEuclideanDistance, geoExtent, - geoInterp, - geoRotate + geoRotate, + geoVecInterp, + geoVecLength } from '../geo'; import { utilGetAllNodes } from '../util'; @@ -69,7 +69,7 @@ export function actionReflect(reflectIds, projection) { q2 = [(ssr.poly[1][0] + ssr.poly[2][0]) / 2, (ssr.poly[1][1] + ssr.poly[2][1]) / 2 ], p, q; - var isLong = (geoEuclideanDistance(p1, q1) > geoEuclideanDistance(p2, q2)); + var isLong = (geoVecLength(p1, q1) > geoVecLength(p2, q2)); if ((useLongAxis && isLong) || (!useLongAxis && !isLong)) { p = p1; q = q1; @@ -92,7 +92,7 @@ export function actionReflect(reflectIds, projection) { b * (c[0] - p[0]) - a * (c[1] - p[1]) + p[1] ]; var loc2 = projection.invert(c2); - node = node.move(geoInterp(node.loc, loc2, t)); + node = node.move(geoVecInterp(node.loc, loc2, t)); graph = graph.replace(node); } diff --git a/modules/actions/reverse.js b/modules/actions/reverse.js index 051f3e64b..3f92fcd91 100644 --- a/modules/actions/reverse.js +++ b/modules/actions/reverse.js @@ -87,7 +87,8 @@ export function actionReverse(wayId, options) { // Update the direction based tags as appropriate then return an updated node return node.update({tags: _transform(node.tags, function(acc, tagValue, tagKey) { // See if this is a direction tag and reverse (or use existing value if not recognised) - if (tagKey.match(/direction$/) !== null) { + var re = /direction$/; + if (re.test(tagKey)) { acc[tagKey] = {forward: 'backward', backward: 'forward', left: 'right', right: 'left'}[tagValue] || tagValue; } else { // Use the reverseKey method to cater for situations such as traffic_sign:forward=stop diff --git a/modules/actions/straighten.js b/modules/actions/straighten.js index e16bdd613..e9456d220 100644 --- a/modules/actions/straighten.js +++ b/modules/actions/straighten.js @@ -1,8 +1,8 @@ import { actionDeleteNode } from './delete_node'; import { - geoEuclideanDistance, - geoInterp + geoVecInterp, + geoVecLength } from '../geo'; @@ -44,7 +44,7 @@ export function actionStraighten(wayId, projection) { ], loc2 = projection.invert(p); - graph = graph.replace(node.move(geoInterp(node.loc, loc2, t))); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc2, t))); } else { // safe to delete @@ -69,7 +69,7 @@ export function actionStraighten(wayId, projection) { points = nodes.map(function(n) { return projection(n.loc); }), startPoint = points[0], endPoint = points[points.length-1], - threshold = 0.2 * geoEuclideanDistance(startPoint, endPoint), + threshold = 0.2 * geoVecLength(startPoint, endPoint), i; if (threshold === 0) { diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index ebd2ea706..f32c952ce 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -33,17 +33,18 @@ import { */ export function behaviorDrag() { - var event = d3_dispatch('start', 'move', 'end'), - origin = null, - selector = '', - filter = null, - event_, target, surface; + var dispatch = d3_dispatch('start', 'move', 'end'); + var _origin = null; + var _selector = ''; + var _event; + var _target; + var _surface; - var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect'), - d3_event_userSelectSuppress = function() { - var selection = d3_selection(), - select = selection.style(d3_event_userSelectProperty); + var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect'); + var d3_event_userSelectSuppress = function() { + var selection = d3_selection(); + var select = selection.style(d3_event_userSelectProperty); selection.style(d3_event_userSelectProperty, 'none'); return function() { selection.style(d3_event_userSelectProperty, select); @@ -60,29 +61,29 @@ export function behaviorDrag() { function eventOf(thiz, argumentz) { return function(e1) { e1.target = drag; - d3_customEvent(e1, event.apply, event, [e1.type, thiz, argumentz]); + d3_customEvent(e1, dispatch.apply, dispatch, [e1.type, thiz, argumentz]); }; } function dragstart() { - target = this; - event_ = eventOf(target, arguments); + _target = this; + _event = eventOf(_target, arguments); - var eventTarget = d3_event.target, - touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null, - offset, - origin_ = point(), - started = false, - selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag'); + var eventTarget = d3_event.target; + var touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null; + var offset; + var startOrigin = point(); + var started = false; + var selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag'); d3_select(window) .on(touchId !== null ? 'touchmove.drag-' + touchId : 'mousemove.drag', dragmove) .on(touchId !== null ? 'touchend.drag-' + touchId : 'mouseup.drag', dragend, true); - if (origin) { - offset = origin.apply(target, arguments); - offset = [offset[0] - origin_[0], offset[1] - origin_[1]]; + if (_origin) { + offset = _origin.apply(_target, arguments); + offset = [offset[0] - startOrigin[0], offset[1] - startOrigin[1]]; } else { offset = [0, 0]; } @@ -93,7 +94,7 @@ export function behaviorDrag() { function point() { - var p = surface || target.parentNode; + var p = _surface || _target.parentNode; return touchId !== null ? d3_touches(p).filter(function(p) { return p.identifier === touchId; })[0] : d3_mouse(p); @@ -101,32 +102,32 @@ export function behaviorDrag() { function dragmove() { - var p = point(), - dx = p[0] - origin_[0], - dy = p[1] - origin_[1]; + var p = point(); + var dx = p[0] - startOrigin[0]; + var dy = p[1] - startOrigin[1]; if (dx === 0 && dy === 0) return; - if (!started) { - started = true; - event_({ type: 'start' }); - } - - origin_ = p; + startOrigin = p; d3_eventCancel(); - event_({ - type: 'move', - point: [p[0] + offset[0], p[1] + offset[1]], - delta: [dx, dy] - }); + if (!started) { + started = true; + _event({ type: 'start' }); + } else { + _event({ + type: 'move', + point: [p[0] + offset[0], p[1] + offset[1]], + delta: [dx, dy] + }); + } } function dragend() { if (started) { - event_({ type: 'end' }); + _event({ type: 'end' }); d3_eventCancel(); if (d3_event.target === eventTarget) { @@ -152,52 +153,46 @@ export function behaviorDrag() { function drag(selection) { - var matchesSelector = utilPrefixDOMProperty('matchesSelector'), - delegate = dragstart; + var matchesSelector = utilPrefixDOMProperty('matchesSelector'); + var delegate = dragstart; - if (selector) { + if (_selector) { delegate = function() { - var root = this, - target = d3_event.target; + var root = this; + var target = d3_event.target; for (; target && target !== root; target = target.parentNode) { - if (target[matchesSelector](selector) && - (!filter || filter(target.__data__))) { - return dragstart.call(target, target.__data__); + var datum = target.__data__; + var entity = datum && datum.properties && datum.properties.entity; + if (entity && target[matchesSelector](_selector)) { + return dragstart.call(target, entity); } } }; } selection - .on('mousedown.drag' + selector, delegate) - .on('touchstart.drag' + selector, delegate); + .on('mousedown.drag' + _selector, delegate) + .on('touchstart.drag' + _selector, delegate); } drag.off = function(selection) { selection - .on('mousedown.drag' + selector, null) - .on('touchstart.drag' + selector, null); + .on('mousedown.drag' + _selector, null) + .on('touchstart.drag' + _selector, null); }; drag.selector = function(_) { - if (!arguments.length) return selector; - selector = _; - return drag; - }; - - - drag.filter = function(_) { - if (!arguments.length) return origin; - filter = _; + if (!arguments.length) return _selector; + _selector = _; return drag; }; drag.origin = function (_) { - if (!arguments.length) return origin; - origin = _; + if (!arguments.length) return _origin; + _origin = _; return drag; }; @@ -211,19 +206,19 @@ export function behaviorDrag() { drag.target = function() { - if (!arguments.length) return target; - target = arguments[0]; - event_ = eventOf(target, Array.prototype.slice.call(arguments, 1)); + if (!arguments.length) return _target; + _target = arguments[0]; + _event = eventOf(_target, Array.prototype.slice.call(arguments, 1)); return drag; }; drag.surface = function() { - if (!arguments.length) return surface; - surface = arguments[0]; + if (!arguments.length) return _surface; + _surface = arguments[0]; return drag; }; - return utilRebind(drag, event, 'on'); + return utilRebind(drag, dispatch, 'on'); } diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 62c6031f6..5866d3c26 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -14,40 +14,52 @@ import { behaviorTail } from './tail'; import { geoChooseEdge, - geoEuclideanDistance + geoVecLength, + geoViewportEdge } from '../geo'; import { utilRebind } from '../util/rebind'; -var usedTails = {}; -var disableSpace = false; -var lastSpace = null; +var _usedTails = {}; +var _disableSpace = false; +var _lastSpace = null; export function behaviorDraw(context) { - var dispatch = d3_dispatch('move', 'click', 'clickWay', - 'clickNode', 'undo', 'cancel', 'finish'), - keybinding = d3_keybinding('draw'), - hover = behaviorHover(context) - .altDisables(true) - .on('hover', context.ui().sidebar.hover), - tail = behaviorTail(), - edit = behaviorEdit(context), - closeTolerance = 4, - tolerance = 12, - mouseLeave = false, - lastMouse = null; + var dispatch = d3_dispatch( + 'move', 'click', 'clickWay', 'clickNode', 'undo', 'cancel', 'finish' + ); + + var keybinding = d3_keybinding('draw'); + + var hover = behaviorHover(context).altDisables(true) + .on('hover', context.ui().sidebar.hover); + var tail = behaviorTail(); + var edit = behaviorEdit(context); + + var closeTolerance = 4; + var tolerance = 12; + var _mouseLeave = false; + var _lastMouse = null; + // related code + // - `mode/drag_node.js` `datum()` function datum() { if (d3_event.altKey) return {}; + var element; if (d3_event.type === 'keydown') { - return (lastMouse && lastMouse.target.__data__) || {}; + element = _lastMouse && _lastMouse.target; } else { - return d3_event.target.__data__ || {}; + element = d3_event.target; } + + // When drawing, snap only to touch targets.. + // (this excludes area fills and active drawing elements) + var d = element.__data__; + return (d && d.properties && d.properties.target) ? d : {}; } @@ -60,17 +72,17 @@ export function behaviorDraw(context) { })[0] : d3_mouse(p); } - var element = d3_select(this), - touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null, - t1 = +new Date(), - p1 = point(); + var element = d3_select(this); + var touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null; + var t1 = +new Date(); + var p1 = point(); element.on('mousemove.draw', null); d3_select(window).on('mouseup.draw', function() { - var t2 = +new Date(), - p2 = point(), - dist = geoEuclideanDistance(p1, p2); + var t2 = +new Date(); + var p2 = point(); + var dist = geoVecLength(p1, p2); element.on('mousemove.draw', mousemove); d3_select(window).on('mouseup.draw', null); @@ -95,44 +107,48 @@ export function behaviorDraw(context) { function mousemove() { - lastMouse = d3_event; + _lastMouse = d3_event; dispatch.call('move', this, datum()); } function mouseenter() { - mouseLeave = false; + _mouseLeave = false; } function mouseleave() { - mouseLeave = true; + _mouseLeave = true; } + // related code + // - `mode/drag_node.js` `doMode()` + // - `behavior/draw.js` `click()` + // - `behavior/draw_way.js` `move()` function click() { var d = datum(); - if (d.type === 'way') { - var dims = context.map().dimensions(), - mouse = context.mouse(), - pad = 5, - trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad && - mouse[1] > pad && mouse[1] < dims[1] - pad; + var target = d && d.id && context.hasEntity(d.id); - if (trySnap) { - var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection), - edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; - dispatch.call('clickWay', this, choice.loc, edge); - } else { - dispatch.call('click', this, context.map().mouseCoordinates()); + var trySnap = geoViewportEdge(context.mouse(), context.map().dimensions()) === null; + if (trySnap) { + if (target && target.type === 'node') { // Snap to a node + dispatch.call('clickNode', this, target); + return; + + } else if (target && target.type === 'way') { // Snap to a way + var choice = geoChooseEdge( + context.childNodes(target), context.mouse(), context.projection, context.activeID() + ); + if (choice) { + var edge = [target.nodes[choice.index - 1], target.nodes[choice.index]]; + dispatch.call('clickWay', this, choice.loc, edge); + return; + } } - - } else if (d.type === 'node') { - dispatch.call('clickNode', this, d); - - } else { - dispatch.call('click', this, context.map().mouseCoordinates()); } + + dispatch.call('click', this, context.map().mouseCoordinates(), d); } @@ -141,23 +157,23 @@ export function behaviorDraw(context) { d3_event.stopPropagation(); var currSpace = context.mouse(); - if (disableSpace && lastSpace) { - var dist = geoEuclideanDistance(lastSpace, currSpace); + if (_disableSpace && _lastSpace) { + var dist = geoVecLength(_lastSpace, currSpace); if (dist > tolerance) { - disableSpace = false; + _disableSpace = false; } } - if (disableSpace || mouseLeave || !lastMouse) return; + if (_disableSpace || _mouseLeave || !_lastMouse) return; // user must move mouse or release space bar to allow another click - lastSpace = currSpace; - disableSpace = true; + _lastSpace = currSpace; + _disableSpace = true; d3_select(window).on('keyup.space-block', function() { d3_event.preventDefault(); d3_event.stopPropagation(); - disableSpace = false; + _disableSpace = false; d3_select(window).on('keyup.space-block', null); }); @@ -187,7 +203,7 @@ export function behaviorDraw(context) { context.install(hover); context.install(edit); - if (!context.inIntro() && !usedTails[tail.text()]) { + if (!context.inIntro() && !_usedTails[tail.text()]) { context.install(tail); } @@ -217,9 +233,9 @@ export function behaviorDraw(context) { context.uninstall(hover); context.uninstall(edit); - if (!context.inIntro() && !usedTails[tail.text()]) { + if (!context.inIntro() && !_usedTails[tail.text()]) { context.uninstall(tail); - usedTails[tail.text()] = true; + _usedTails[tail.text()] = true; } selection diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 57ecfb193..6c43c6ea9 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -1,97 +1,87 @@ -import _clone from 'lodash-es/clone'; - import { t } from '../util/locale'; import { - actionAddEntity, actionAddMidpoint, actionMoveNode, actionNoop } from '../actions'; import { behaviorDraw } from './draw'; - -import { - geoChooseEdge, - geoEdgeEqual -} from '../geo'; - -import { - modeBrowse, - modeSelect -} from '../modes'; - -import { - osmNode, - osmWay -} from '../osm'; - -import { utilEntitySelector } from '../util'; +import { geoChooseEdge, geoHasSelfIntersections } from '../geo'; +import { modeBrowse, modeSelect } from '../modes'; +import { osmNode } from '../osm'; export function behaviorDrawWay(context, wayId, index, mode, startGraph) { + var origWay = context.entity(wayId); + var annotation = t((origWay.isDegenerate() ? + 'operations.start.annotation.' : + 'operations.continue.annotation.') + context.geometry(wayId) + ); + var behavior = behaviorDraw(context); + var _tempEdits = 0; - var origWay = context.entity(wayId), - isArea = context.geometry(wayId) === 'area', - tempEdits = 0, - annotation = t((origWay.isDegenerate() ? - 'operations.start.annotation.' : - 'operations.continue.annotation.') + context.geometry(wayId)), - draw = behaviorDraw(context), - startIndex, - start, - end, - segment; - - - // initialize the temporary drawing entities - if (!isArea) { - startIndex = typeof index === 'undefined' ? origWay.nodes.length - 1 : 0; - start = osmNode({ id: 'nStart', loc: context.entity(origWay.nodes[startIndex]).loc }); - end = osmNode({ id: 'nEnd', loc: context.map().mouseCoordinates() }); - segment = osmWay({ id: 'wTemp', - nodes: typeof index === 'undefined' ? [start.id, end.id] : [end.id, start.id], - tags: _clone(origWay.tags) - }); - } else { - end = osmNode({ loc: context.map().mouseCoordinates() }); - } + var end = osmNode({ loc: context.map().mouseCoordinates() }); // Push an annotated state for undo to return back to. // We must make sure to remove this edit later. context.perform(actionNoop(), annotation); - tempEdits++; + _tempEdits++; - // Add the temporary drawing entities to the graph. + // Add the drawing node to the graph. // We must make sure to remove this edit later. - context.perform(AddDrawEntities()); - tempEdits++; + context.perform(_actionAddDrawNode()); + _tempEdits++; + // related code + // - `mode/drag_node.js` `doMode()` + // - `behavior/draw.js` `click()` + // - `behavior/draw_way.js` `move()` function move(datum) { - var loc; + var nodeLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc; + var nodeGroups = datum && datum.properties && datum.properties.nodes; + var loc = context.map().mouseCoordinates(); - if (datum.type === 'node' && datum.id !== end.id) { - loc = datum.loc; + if (nodeLoc) { // snap to node/vertex - a point target with `.loc` + loc = nodeLoc; - } else if (datum.type === 'way') { - var dims = context.map().dimensions(), - mouse = context.mouse(), - pad = 5, - trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad && - mouse[1] > pad && mouse[1] < dims[1] - pad; - - if (trySnap) { - loc = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc; + } else if (nodeGroups) { // snap to way - a line target with `.nodes` + var best = Infinity; + for (var i = 0; i < nodeGroups.length; i++) { + var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); + var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, end.id); + if (choice && choice.distance < best) { + best = choice.distance; + loc = choice.loc; + } } } - if (!loc) { - loc = context.map().mouseCoordinates(); - } - context.replace(actionMoveNode(end.id, loc)); end = context.entity(end.id); + + // check if this movement causes the geometry to break + var doBlock = invalidGeometry(end, context.graph()); + context.surface() + .classed('nope', doBlock); + } + + + function invalidGeometry(entity, graph) { + var parents = graph.parentWays(entity); + + for (var i = 0; i < parents.length; i++) { + var parent = parents[i]; + var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); + if (parent.isClosed()) { + if (geoHasSelfIntersections(nodes, entity.id)) { + return true; + } + } + } + + return false; } @@ -99,7 +89,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Undo popped the history back to the initial annotated no-op edit. // Remove initial no-op edit and whatever edit happened immediately before it. context.pop(2); - tempEdits = 0; + _tempEdits = 0; if (context.hasEntity(wayId)) { context.enter(mode); @@ -110,14 +100,14 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function setActiveElements() { - var active = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; - context.surface().selectAll(utilEntitySelector(active)) + context.surface().selectAll('.' + end.id) .classed('active', true); } var drawWay = function(surface) { - draw.on('move', move) + behavior + .on('move', move) .on('click', drawWay.add) .on('clickWay', drawWay.addWay) .on('clickNode', drawWay.addNode) @@ -131,7 +121,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { setActiveElements(); - surface.call(draw); + surface.call(behavior); context.history() .on('undone.draw', undone); @@ -142,8 +132,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Drawing was interrupted unexpectedly. // This can happen if the user changes modes, // clicks geolocate button, a hashchange event occurs, etc. - if (tempEdits) { - context.pop(tempEdits); + if (_tempEdits) { + context.pop(_tempEdits); while (context.graph() !== startGraph) { context.pop(); } @@ -152,7 +142,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.map() .on('drawn.draw', null); - surface.call(draw.off) + surface.call(behavior.off) .selectAll('.active') .classed('active', false); @@ -161,129 +151,75 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { }; - function AddDrawEntities() { + function _actionAddDrawNode() { return function(graph) { - if (isArea) { - // For area drawing, there is no need for a temporary node. - // `end` gets inserted into the way as the penultimate node. - return graph - .replace(end) - .replace(origWay.addNode(end.id)); - } else { - // For line drawing, add a temporary start, end, and segment to the graph. - // This allows us to class the new segment as `active`, but still - // connect it back to parts of the way that have already been drawn. - return graph - .replace(start) - .replace(end) - .replace(segment); - } + return graph + .replace(end) + .replace(origWay.addNode(end.id, index)); }; } - function ReplaceDrawEntities(newNode) { + function _actionReplaceDrawNode(newNode) { return function(graph) { - if (isArea) { - // For area drawing, we didn't create a temporary node. - // `newNode` gets inserted into the _original_ way as the penultimate node. - return graph - .replace(origWay.addNode(newNode.id)) - .remove(end); - } else { - // For line drawing, add the `newNode` to the way at specified index, - // and remove the temporary start, end, and segment. - return graph - .replace(origWay.addNode(newNode.id, index)) - .remove(end) - .remove(segment) - .remove(start); - } + return graph + .replace(origWay.addNode(newNode.id, index)) + .remove(end); }; } - // Accept the current position of the temporary node and continue drawing. - drawWay.add = function(loc) { - // prevent duplicate nodes - var last = context.hasEntity(origWay.nodes[origWay.nodes.length - (isArea ? 2 : 1)]); - if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return; - - context.pop(tempEdits); - - if (isArea) { - context.perform( - AddDrawEntities(), - annotation - ); - } else { - var newNode = osmNode({loc: loc}); - context.perform( - actionAddEntity(newNode), - ReplaceDrawEntities(newNode), - annotation - ); + // Accept the current position of the drawing node and continue drawing. + drawWay.add = function(loc, d) { + if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) { + return; // can't click here } - tempEdits = 0; + context.pop(_tempEdits); + _tempEdits = 0; + + context.perform( + _actionAddDrawNode(), + annotation + ); + context.enter(mode); }; // Connect the way to an existing way. drawWay.addWay = function(loc, edge) { - if (isArea) { - context.pop(tempEdits); - - context.perform( - AddDrawEntities(), - actionAddMidpoint({ loc: loc, edge: edge}, end), - annotation - ); - } else { - var previousEdge = startIndex ? - [origWay.nodes[startIndex], origWay.nodes[startIndex - 1]] : - [origWay.nodes[0], origWay.nodes[1]]; - - // Avoid creating duplicate segments - if (geoEdgeEqual(edge, previousEdge)) - return; - - context.pop(tempEdits); - - var newNode = osmNode({ loc: loc }); - context.perform( - actionAddMidpoint({ loc: loc, edge: edge}, newNode), - ReplaceDrawEntities(newNode), - annotation - ); + if (context.surface().classed('nope')) { + return; // can't click here } - tempEdits = 0; + context.pop(_tempEdits); + _tempEdits = 0; + + context.perform( + _actionAddDrawNode(), + actionAddMidpoint({ loc: loc, edge: edge }, end), + annotation + ); + context.enter(mode); }; // Connect the way to an existing node and continue drawing. drawWay.addNode = function(node) { - // Avoid creating duplicate segments - if (origWay.areAdjacent(node.id, origWay.nodes[origWay.nodes.length - 1])) return; - - // Clicks should not occur on the drawing node, however a space keypress can - // sometimes grab that node's datum (before it gets classed as `active`?) #4016 - if (node.id === end.id) { - drawWay.add(node.loc); - return; + if (context.surface().classed('nope')) { + return; // can't click here } - context.pop(tempEdits); + context.pop(_tempEdits); + _tempEdits = 0; context.perform( - ReplaceDrawEntities(node), + _actionReplaceDrawNode(node), annotation ); - tempEdits = 0; context.enter(mode); }; @@ -292,8 +228,12 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // If the way has enough nodes to be valid, it's selected. // Otherwise, delete everything and return to browse mode. drawWay.finish = function() { - context.pop(tempEdits); - tempEdits = 0; + if (context.surface().classed('nope')) { + return; // can't click here + } + + context.pop(_tempEdits); + _tempEdits = 0; var way = context.hasEntity(wayId); if (!way || way.isDegenerate()) { @@ -311,8 +251,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Cancel the draw operation, delete everything, and return to browse mode. drawWay.cancel = function() { - context.pop(tempEdits); - tempEdits = 0; + context.pop(_tempEdits); + _tempEdits = 0; while (context.graph() !== startGraph) { context.pop(); @@ -322,12 +262,22 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.map().dblclickEnable(true); }, 1000); + context.surface() + .classed('nope', false); + context.enter(modeBrowse(context)); }; + drawWay.activeID = function() { + if (!arguments.length) return end.id; + // no assign + return drawWay; + }; + + drawWay.tail = function(text) { - draw.tail(text); + behavior.tail(text); return drawWay; }; diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index f7ea0b4eb..e57687369 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -6,7 +6,7 @@ import { } from 'd3-selection'; import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; -import { osmEntity } from '../osm/index'; +import { osmEntity } from '../osm'; import { utilRebind } from '../util/rebind'; @@ -20,16 +20,16 @@ import { utilRebind } from '../util/rebind'; have the .hover class. */ export function behaviorHover(context) { - var dispatch = d3_dispatch('hover'), - _selection = d3_select(null), - newId = null, - buttonDown, - altDisables, - target; + var dispatch = d3_dispatch('hover'); + var _selection = d3_select(null); + var _newId = null; + var _buttonDown; + var _altDisables; + var _target; function keydown() { - if (altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { + if (_altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { _selection.selectAll('.hover') .classed('hover-suppressed', true) .classed('hover', false); @@ -43,7 +43,7 @@ export function behaviorHover(context) { function keyup() { - if (altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { + if (_altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false) .classed('hover', true); @@ -51,14 +51,14 @@ export function behaviorHover(context) { _selection .classed('hover-disabled', false); - dispatch.call('hover', this, target ? target.id : null); + dispatch.call('hover', this, _target ? _target.id : null); } } var hover = function(selection) { _selection = selection; - newId = null; + _newId = null; _selection .on('mouseover.hover', mouseover) @@ -71,65 +71,71 @@ export function behaviorHover(context) { function mouseover() { - if (buttonDown) return; + if (_buttonDown) return; var target = d3_event.target; enter(target ? target.__data__ : null); } function mouseout() { - if (buttonDown) return; + if (_buttonDown) return; var target = d3_event.relatedTarget; enter(target ? target.__data__ : null); } function mousedown() { - buttonDown = true; + _buttonDown = true; d3_select(window) .on('mouseup.hover', mouseup, true); } function mouseup() { - buttonDown = false; + _buttonDown = false; d3_select(window) .on('mouseup.hover', null, true); } - function enter(d) { - if (d === target) return; - target = d; + function enter(datum) { + if (datum === _target) return; + _target = datum; _selection.selectAll('.hover') .classed('hover', false); _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - if (target instanceof osmEntity && target.id !== newId) { + var entity; + if (datum instanceof osmEntity) { + entity = datum; + } else { + entity = datum && datum.properties && datum.properties.entity; + } + if (entity && entity.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 var mode = context.mode() && context.mode().id; - if ((mode === 'draw-line' || mode === 'draw-area') && !newId && target.type === 'node') { - newId = target.id; + if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && entity.type === 'node') { + _newId = entity.id; return; } - var selector = '.' + target.id; + var selector = '.' + entity.id; - if (target.type === 'relation') { - target.members.forEach(function(member) { + if (entity.type === 'relation') { + entity.members.forEach(function(member) { selector += ', .' + member.id; }); } - var suppressed = altDisables && d3_event && d3_event.altKey; + var suppressed = _altDisables && d3_event && d3_event.altKey; _selection.selectAll(selector) .classed(suppressed ? 'hover-suppressed' : 'hover', true); - dispatch.call('hover', this, !suppressed && target.id); + dispatch.call('hover', this, !suppressed && entity.id); } else { dispatch.call('hover', this, null); @@ -147,7 +153,6 @@ export function behaviorHover(context) { selection .classed('hover-disabled', false); - selection .on('mouseover.hover', null) .on('mouseout.hover', null) @@ -160,8 +165,8 @@ export function behaviorHover(context) { hover.altDisables = function(_) { - if (!arguments.length) return altDisables; - altDisables = _; + if (!arguments.length) return _altDisables; + _altDisables = _; return hover; }; diff --git a/modules/behavior/select.js b/modules/behavior/select.js index f3daf0bc2..9a9c0b34d 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -6,7 +6,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoEuclideanDistance } from '../geo'; +import { geoVecLength } from '../geo'; import { modeBrowse, @@ -17,10 +17,10 @@ import { osmEntity } from '../osm'; export function behaviorSelect(context) { - var lastMouse = null, - suppressMenu = true, - tolerance = 4, - p1 = null; + var lastMouse = null; + var suppressMenu = true; + var tolerance = 4; + var p1 = null; function point() { @@ -102,19 +102,21 @@ export function behaviorSelect(context) { .on('mouseup.select', null, true); if (!p1) return; - var p2 = point(), - dist = geoEuclideanDistance(p1, p2); + var p2 = point(); + var dist = geoVecLength(p1, p2); p1 = null; if (dist > tolerance) { return; } - var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node(), - isShowAlways = +context.storage('edit-menu-show-always') === 1, - datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__), - mode = context.mode(); + var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node(); + var isShowAlways = +context.storage('edit-menu-show-always') === 1; + var datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__); + var mode = context.mode(); + var entity = datum && datum.properties && datum.properties.entity; + if (entity) datum = entity; if (datum && datum.type === 'midpoint') { datum = datum.parents[0]; diff --git a/modules/core/context.js b/modules/core/context.js index ce0cf5787..1f606d5fe 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -255,6 +255,9 @@ export function coreContext() { return []; } }; + context.activeID = function() { + return mode && mode.activeID && mode.activeID(); + }; /* Behaviors */ @@ -310,11 +313,12 @@ export function coreContext() { /* Debug */ var debugFlags = { - tile: false, - collision: false, - imagery: false, - imperial: false, - driveLeft: false + tile: false, // tile boundaries + collision: false, // label collision bounding boxes + imagery: false, // imagery bounding polygons + imperial: false, // imperial (not metric) bounding polygons + driveLeft: false, // driveLeft bounding polygons + target: false // touch targets }; context.debugFlags = function() { return debugFlags; diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 3d083d219..639887bac 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -1,277 +1,70 @@ -import _every from 'lodash-es/every'; -import _some from 'lodash-es/some'; +// constants +var TAU = 2 * Math.PI; +var EQUATORIAL_RADIUS = 6356752.314245179; +var POLAR_RADIUS = 6378137.0; -export function geoRoundCoords(c) { - return [Math.floor(c[0]), Math.floor(c[1])]; -} - - -export function geoInterp(p1, p2, t) { - return [p1[0] + (p2[0] - p1[0]) * t, - p1[1] + (p2[1] - p1[1]) * t]; -} - - -// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. -// Returns a positive value, if OAB makes a counter-clockwise turn, -// negative for clockwise turn, and zero if the points are collinear. -export function geoCross(o, a, b) { - return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); -} - - -// http://jsperf.com/id-dist-optimization -export function geoEuclideanDistance(a, b) { - var x = a[0] - b[0], y = a[1] - b[1]; - return Math.sqrt((x * x) + (y * y)); -} - - -// using WGS84 polar radius (6356752.314245179 m) -// const = 2 * PI * r / 360 export function geoLatToMeters(dLat) { - return dLat * 110946.257617; + return dLat * (TAU * POLAR_RADIUS / 360); } -// using WGS84 equatorial radius (6378137.0 m) -// const = 2 * PI * r / 360 export function geoLonToMeters(dLon, atLat) { return Math.abs(atLat) >= 90 ? 0 : - dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180))); + dLon * (TAU * EQUATORIAL_RADIUS / 360) * Math.abs(Math.cos(atLat * (Math.PI / 180))); } -// using WGS84 polar radius (6356752.314245179 m) -// const = 2 * PI * r / 360 export function geoMetersToLat(m) { - return m / 110946.257617; + return m / (TAU * POLAR_RADIUS / 360); } -// using WGS84 equatorial radius (6378137.0 m) -// const = 2 * PI * r / 360 export function geoMetersToLon(m, atLat) { return Math.abs(atLat) >= 90 ? 0 : - m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180))); + m / (TAU * EQUATORIAL_RADIUS / 360) / Math.abs(Math.cos(atLat * (Math.PI / 180))); } -export function geoOffsetToMeters(offset) { - var equatRadius = 6356752.314245179, - polarRadius = 6378137.0, - tileSize = 256; - +export function geoMetersToOffset(meters, tileSize) { + tileSize = tileSize || 256; return [ - offset[0] * 2 * Math.PI * equatRadius / tileSize, - -offset[1] * 2 * Math.PI * polarRadius / tileSize + meters[0] * tileSize / (TAU * EQUATORIAL_RADIUS), + -meters[1] * tileSize / (TAU * POLAR_RADIUS) ]; } -export function geoMetersToOffset(meters) { - var equatRadius = 6356752.314245179, - polarRadius = 6378137.0, - tileSize = 256; - +export function geoOffsetToMeters(offset, tileSize) { + tileSize = tileSize || 256; return [ - meters[0] * tileSize / (2 * Math.PI * equatRadius), - -meters[1] * tileSize / (2 * Math.PI * polarRadius) + offset[0] * TAU * EQUATORIAL_RADIUS / tileSize, + -offset[1] * TAU * POLAR_RADIUS / tileSize ]; } // Equirectangular approximation of spherical distances on Earth export function geoSphericalDistance(a, b) { - var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2), - y = geoLatToMeters(a[1] - b[1]); + var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2); + var y = geoLatToMeters(a[1] - b[1]); return Math.sqrt((x * x) + (y * y)); } -export function geoEdgeEqual(a, b) { - return (a[0] === b[0] && a[1] === b[1]) || - (a[0] === b[1] && a[1] === b[0]); +// scale to zoom +export function geoScaleToZoom(k, tileSize) { + tileSize = tileSize || 256; + var log2ts = Math.log(tileSize) * Math.LOG2E; + return Math.log(k * TAU) / Math.LN2 - log2ts; } -// Return the counterclockwise angle in the range (-pi, pi) -// between the positive X axis and the line intersecting a and b. -export function geoAngle(a, b, projection) { - a = projection(a.loc); - b = projection(b.loc); - return Math.atan2(b[1] - a[1], b[0] - a[0]); +// zoom to scale +export function geoZoomToScale(z, tileSize) { + tileSize = tileSize || 256; + return tileSize * Math.pow(2, z) / TAU; } -// Rotate all points counterclockwise around a pivot point by given angle -export function geoRotate(points, angle, around) { - return points.map(function(point) { - var radial = [point[0] - around[0], point[1] - around[1]]; - return [ - radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], - radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] - ]; - }); -} - -// Choose the edge with the minimal distance from `point` to its orthogonal -// projection onto that edge, if such a projection exists, or the distance to -// the closest vertex on that edge. Returns an object with the `index` of the -// chosen edge, the chosen `loc` on that edge, and the `distance` to to it. -export function geoChooseEdge(nodes, point, projection) { - var dist = geoEuclideanDistance, - points = nodes.map(function(n) { return projection(n.loc); }), - min = Infinity, - idx, loc; - - function dot(p, q) { - return p[0] * q[0] + p[1] * q[1]; - } - - for (var i = 0; i < points.length - 1; i++) { - var o = points[i], - s = [points[i + 1][0] - o[0], - points[i + 1][1] - o[1]], - v = [point[0] - o[0], - point[1] - o[1]], - proj = dot(v, s) / dot(s, s), - p; - - if (proj < 0) { - p = o; - } else if (proj > 1) { - p = points[i + 1]; - } else { - p = [o[0] + proj * s[0], o[1] + proj * s[1]]; - } - - var d = dist(p, point); - if (d < min) { - min = d; - idx = i + 1; - loc = projection.invert(p); - } - } - - return { - index: idx, - distance: min, - loc: loc - }; -} - - -// Return the intersection point of 2 line segments. -// From https://github.com/pgkelley4/line-segments-intersect -// This uses the vector cross product approach described below: -// http://stackoverflow.com/a/565282/786339 -export function geoLineIntersection(a, b) { - function subtractPoints(point1, point2) { - return [point1[0] - point2[0], point1[1] - point2[1]]; - } - function crossProduct(point1, point2) { - return point1[0] * point2[1] - point1[1] * point2[0]; - } - - var p = [a[0][0], a[0][1]], - p2 = [a[1][0], a[1][1]], - q = [b[0][0], b[0][1]], - q2 = [b[1][0], b[1][1]], - r = subtractPoints(p2, p), - s = subtractPoints(q2, q), - uNumerator = crossProduct(subtractPoints(q, p), r), - denominator = crossProduct(r, s); - - if (uNumerator && denominator) { - var u = uNumerator / denominator, - t = crossProduct(subtractPoints(q, p), s) / denominator; - - if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { - return geoInterp(p, p2, t); - } - } - - return null; -} - - -export function geoPathIntersections(path1, path2) { - var intersections = []; - for (var i = 0; i < path1.length - 1; i++) { - for (var j = 0; j < path2.length - 1; j++) { - var a = [ path1[i], path1[i+1] ], - b = [ path2[j], path2[j+1] ], - hit = geoLineIntersection(a, b); - if (hit) intersections.push(hit); - } - } - return intersections; -} - - -// Return whether point is contained in polygon. -// -// `point` should be a 2-item array of coordinates. -// `polygon` should be an array of 2-item arrays of coordinates. -// -// From https://github.com/substack/point-in-polygon. -// ray-casting algorithm based on -// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html -// -export function geoPointInPolygon(point, polygon) { - var x = point[0], - y = point[1], - inside = false; - - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - - var intersect = ((yi > y) !== (yj > y)) && - (x < (xj - xi) * (y - yi) / (yj - yi) + xi); - if (intersect) inside = !inside; - } - - return inside; -} - - -export function geoPolygonContainsPolygon(outer, inner) { - return _every(inner, function(point) { - return geoPointInPolygon(point, outer); - }); -} - - -export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { - function testSegments(outer, inner) { - for (var i = 0; i < outer.length - 1; i++) { - for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i+1] ], - b = [ inner[j], inner[j+1] ]; - if (geoLineIntersection(a, b)) return true; - } - } - return false; - } - - function testPoints(outer, inner) { - return _some(inner, function(point) { - return geoPointInPolygon(point, outer); - }); - } - - return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); -} - - -export function geoPathLength(path) { - var length = 0; - for (var i = 0; i < path.length - 1; i++) { - length += geoEuclideanDistance(path[i], path[i + 1]); - } - return length; -} diff --git a/modules/geo/geom.js b/modules/geo/geom.js new file mode 100644 index 000000000..da76cb607 --- /dev/null +++ b/modules/geo/geom.js @@ -0,0 +1,252 @@ +import _every from 'lodash-es/every'; +import _some from 'lodash-es/some'; + +import { + geoVecAngle, + geoVecCross, + geoVecDot, + geoVecEqual, + geoVecInterp, + geoVecLength, + geoVecSubtract +} from './vector.js'; + + +// Return the counterclockwise angle in the range (-pi, pi) +// between the positive X axis and the line intersecting a and b. +export function geoAngle(a, b, projection) { + return geoVecAngle(projection(a.loc), projection(b.loc)); +} + +export function geoEdgeEqual(a, b) { + return (a[0] === b[0] && a[1] === b[1]) || + (a[0] === b[1] && a[1] === b[0]); +} + +// Rotate all points counterclockwise around a pivot point by given angle +export function geoRotate(points, angle, around) { + return points.map(function(point) { + var radial = geoVecSubtract(point, around); + return [ + radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], + radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] + ]; + }); +} + + +// Choose the edge with the minimal distance from `point` to its orthogonal +// projection onto that edge, if such a projection exists, or the distance to +// the closest vertex on that edge. Returns an object with the `index` of the +// chosen edge, the chosen `loc` on that edge, and the `distance` to to it. +export function geoChooseEdge(nodes, point, projection, activeID) { + var dist = geoVecLength; + var points = nodes.map(function(n) { return projection(n.loc); }); + var ids = nodes.map(function(n) { return n.id; }); + var min = Infinity; + var idx; + var loc; + + for (var i = 0; i < points.length - 1; i++) { + if (ids[i] === activeID || ids[i + 1] === activeID) continue; + + var o = points[i]; + var s = geoVecSubtract(points[i + 1], o); + var v = geoVecSubtract(point, o); + var proj = geoVecDot(v, s) / geoVecDot(s, s); + var p; + + if (proj < 0) { + p = o; + } else if (proj > 1) { + p = points[i + 1]; + } else { + p = [o[0] + proj * s[0], o[1] + proj * s[1]]; + } + + var d = dist(p, point); + if (d < min) { + min = d; + idx = i + 1; + loc = projection.invert(p); + } + } + + if (idx !== undefined) { + return { index: idx, distance: min, loc: loc }; + } else { + return null; + } +} + + +// check active (dragged or drawing) segments against inactive segments +export function geoHasSelfIntersections(nodes, activeID) { + var actives = []; + var inactives = []; + var j, k; + + for (j = 0; j < nodes.length - 1; j++) { + var n1 = nodes[j]; + var n2 = nodes[j+1]; + var segment = [n1.loc, n2.loc]; + if (n1.id === activeID || n2.id === activeID) { + actives.push(segment); + } else { + inactives.push(segment); + } + } + + for (j = 0; j < actives.length; j++) { + for (k = 0; k < inactives.length; k++) { + var p = actives[j]; + var q = inactives[k]; + // skip if segments share an endpoint + if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) || + geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) { + continue; + } else if (geoLineIntersection(p, q)) { + return true; + } + } + } + + return false; +} + + +// Return the intersection point of 2 line segments. +// From https://github.com/pgkelley4/line-segments-intersect +// This uses the vector cross product approach described below: +// http://stackoverflow.com/a/565282/786339 +export function geoLineIntersection(a, b) { + var p = [a[0][0], a[0][1]]; + var p2 = [a[1][0], a[1][1]]; + var q = [b[0][0], b[0][1]]; + var q2 = [b[1][0], b[1][1]]; + var r = geoVecSubtract(p2, p); + var s = geoVecSubtract(q2, q); + var uNumerator = geoVecCross(geoVecSubtract(q, p), r); + var denominator = geoVecCross(r, s); + + if (uNumerator && denominator) { + var u = uNumerator / denominator; + var t = geoVecCross(geoVecSubtract(q, p), s) / denominator; + + if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { + return geoVecInterp(p, p2, t); + } + } + + return null; +} + + +export function geoPathIntersections(path1, path2) { + var intersections = []; + for (var i = 0; i < path1.length - 1; i++) { + for (var j = 0; j < path2.length - 1; j++) { + var a = [ path1[i], path1[i+1] ]; + var b = [ path2[j], path2[j+1] ]; + var hit = geoLineIntersection(a, b); + if (hit) { + intersections.push(hit); + } + } + } + return intersections; +} + +export function geoPathHasIntersections(path1, path2) { + for (var i = 0; i < path1.length - 1; i++) { + for (var j = 0; j < path2.length - 1; j++) { + var a = [ path1[i], path1[i+1] ]; + var b = [ path2[j], path2[j+1] ]; + var hit = geoLineIntersection(a, b); + if (hit) { + return true; + } + } + } + return false; +} + + +// Return whether point is contained in polygon. +// +// `point` should be a 2-item array of coordinates. +// `polygon` should be an array of 2-item arrays of coordinates. +// +// From https://github.com/substack/point-in-polygon. +// ray-casting algorithm based on +// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html +// +export function geoPointInPolygon(point, polygon) { + var x = point[0]; + var y = point[1]; + var inside = false; + + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0]; + var yi = polygon[i][1]; + var xj = polygon[j][0]; + var yj = polygon[j][1]; + + var intersect = ((yi > y) !== (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + if (intersect) inside = !inside; + } + + return inside; +} + + +export function geoPolygonContainsPolygon(outer, inner) { + return _every(inner, function(point) { + return geoPointInPolygon(point, outer); + }); +} + + +export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { + function testPoints(outer, inner) { + return _some(inner, function(point) { + return geoPointInPolygon(point, outer); + }); + } + + return testPoints(outer, inner) || (!!checkSegments && geoPathHasIntersections(outer, inner)); +} + + +export function geoPathLength(path) { + var length = 0; + for (var i = 0; i < path.length - 1; i++) { + length += geoVecLength(path[i], path[i + 1]); + } + return length; +} + + +// If the given point is at the edge of the padded viewport, +// return a vector that will nudge the viewport in that direction +export function geoViewportEdge(point, dimensions) { + var pad = [80, 20, 50, 20]; // top, right, bottom, left + var x = 0; + var y = 0; + + if (point[0] > dimensions[0] - pad[1]) + x = -10; + if (point[0] < pad[3]) + x = 10; + if (point[1] > dimensions[1] - pad[2]) + y = -10; + if (point[1] < pad[0]) + y = 10; + + if (x || y) { + return [x, y]; + } else { + return null; + } +} diff --git a/modules/geo/index.js b/modules/geo/index.js index 5e37b16fe..0c80b12d6 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -1,23 +1,39 @@ -export { geoAngle } from './geo.js'; -export { geoChooseEdge } from './geo.js'; -export { geoCross } from './geo.js'; -export { geoEdgeEqual } from './geo.js'; -export { geoEuclideanDistance } from './geo.js'; export { geoExtent } from './extent.js'; -export { geoInterp } from './geo.js'; -export { geoRawMercator } from './raw_mercator.js'; -export { geoRoundCoords } from './geo.js'; -export { geoRotate } from './geo.js'; + export { geoLatToMeters } from './geo.js'; -export { geoLineIntersection } from './geo.js'; export { geoLonToMeters } from './geo.js'; export { geoMetersToLat } from './geo.js'; export { geoMetersToLon } from './geo.js'; export { geoMetersToOffset } from './geo.js'; export { geoOffsetToMeters } from './geo.js'; -export { geoPathIntersections } from './geo.js'; -export { geoPathLength } from './geo.js'; -export { geoPointInPolygon } from './geo.js'; -export { geoPolygonContainsPolygon } from './geo.js'; -export { geoPolygonIntersectsPolygon } from './geo.js'; +export { geoScaleToZoom } from './geo.js'; export { geoSphericalDistance } from './geo.js'; +export { geoZoomToScale } from './geo.js'; + +export { geoAngle } from './geom.js'; +export { geoChooseEdge } from './geom.js'; +export { geoEdgeEqual } from './geom.js'; +export { geoHasSelfIntersections } from './geom.js'; +export { geoRotate } from './geom.js'; +export { geoLineIntersection } from './geom.js'; +export { geoPathHasIntersections } from './geom.js'; +export { geoPathIntersections } from './geom.js'; +export { geoPathLength } from './geom.js'; +export { geoPointInPolygon } from './geom.js'; +export { geoPolygonContainsPolygon } from './geom.js'; +export { geoPolygonIntersectsPolygon } from './geom.js'; +export { geoViewportEdge } from './geom.js'; + +export { geoRawMercator } from './raw_mercator.js'; + +export { geoVecAdd } from './vector.js'; +export { geoVecAngle } from './vector.js'; +export { geoVecCross } from './vector.js'; +export { geoVecDot } from './vector.js'; +export { geoVecEqual } from './vector.js'; +export { geoVecFloor } from './vector.js'; +export { geoVecInterp } from './vector.js'; +export { geoVecLength } from './vector.js'; +export { geoVecSubtract } from './vector.js'; +export { geoVecScale } from './vector.js'; + diff --git a/modules/geo/vector.js b/modules/geo/vector.js new file mode 100644 index 000000000..0e7929b3b --- /dev/null +++ b/modules/geo/vector.js @@ -0,0 +1,62 @@ +// vector equals +export function geoVecEqual(a, b) { + return (a[0] === b[0]) && (a[1] === b[1]); +} + +// vector addition +export function geoVecAdd(a, b) { + return [ a[0] + b[0], a[1] + b[1] ]; +} + +// vector subtraction +export function geoVecSubtract(a, b) { + return [ a[0] - b[0], a[1] - b[1] ]; +} + +// vector multiplication +export function geoVecScale(a, b) { + return [ a[0] * b, a[1] * b ]; +} + +// vector rounding (was: geoRoundCoordinates) +export function geoVecFloor(a) { + return [ Math.floor(a[0]), Math.floor(a[1]) ]; +} + +// linear interpolation +export function geoVecInterp(a, b, t) { + return [ + a[0] + (b[0] - a[0]) * t, + a[1] + (b[1] - a[1]) * t + ]; +} + +// http://jsperf.com/id-dist-optimization +export function geoVecLength(a, b) { + var x = a[0] - b[0]; + var y = a[1] - b[1]; + return Math.sqrt((x * x) + (y * y)); +} + +// Return the counterclockwise angle in the range (-pi, pi) +// between the positive X axis and the line intersecting a and b. +export function geoVecAngle(a, b) { + return Math.atan2(b[1] - a[1], b[0] - a[0]); +} + +// dot product +export function geoVecDot(a, b, origin) { + origin = origin || [0, 0]; + return (a[0] - origin[0]) * (b[0] - origin[0]) + + (a[1] - origin[1]) * (b[1] - origin[1]); +} + +// 2D cross product of OA and OB vectors, returns magnitude of Z vector +// Returns a positive value, if OAB makes a counter-clockwise turn, +// negative for clockwise turn, and zero if the points are collinear. +export function geoVecCross(a, b, origin) { + origin = origin || [0, 0]; + return (a[0] - origin[0]) * (b[1] - origin[1]) - + (a[1] - origin[1]) * (b[0] - origin[0]); +} + diff --git a/modules/index.js b/modules/index.js index 981a047db..dcd7f276e 100644 --- a/modules/index.js +++ b/modules/index.js @@ -29,6 +29,10 @@ export { coreDifference as Difference } from './core/difference'; export { coreGraph as Graph } from './core/graph'; export { coreHistory as History } from './core/history'; export { coreTree as Tree } from './core/tree'; +export { geoVecCross as geoCross } from './geo/vector'; +export { geoVecInterp as geoInterp } from './geo/vector'; +export { geoVecFloor as geoRoundCoordinates } from './geo/vector'; +export { geoVecLength as geoEuclideanDistance } from './geo/vector'; export { osmEntity as Entity } from './osm/entity'; export { osmNode as Node } from './osm/node'; export { osmRelation as Relation } from './osm/relation'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index baed024ae..faf603f66 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -1,4 +1,4 @@ -import _map from 'lodash-es/map'; +import _find from 'lodash-es/find'; import { event as d3_event, @@ -21,13 +21,15 @@ import { } from '../behavior'; import { - modeBrowse, - modeSelect -} from './index'; + geoChooseEdge, + geoHasSelfIntersections, + geoPathHasIntersections, + geoVecSubtract, + geoViewportEdge +} from '../geo'; -import { geoChooseEdge } from '../geo'; -import { osmNode } from '../osm'; -import { utilEntitySelector } from '../util'; +import { modeBrowse, modeSelect } from './index'; +import { osmJoinWays, osmNode } from '../osm'; import { uiFlash } from '../ui'; @@ -36,46 +38,22 @@ export function modeDragNode(context) { id: 'drag-node', button: 'browse' }; + var hover = behaviorHover(context).altDisables(true) + .on('hover', context.ui().sidebar.hover); + var edit = behaviorEdit(context); - var nudgeInterval, - activeIDs, - wasMidpoint, - isCancelled, - lastLoc, - selectedIDs = [], - hover = behaviorHover(context).altDisables(true).on('hover', context.ui().sidebar.hover), - edit = behaviorEdit(context); - - - function vecSub(a, b) { - return [a[0] - b[0], a[1] - b[1]]; - } - - function edge(point, size) { - var pad = [80, 20, 50, 20], // top, right, bottom, left - x = 0, - y = 0; - - if (point[0] > size[0] - pad[1]) - x = -10; - if (point[0] < pad[3]) - x = 10; - if (point[1] > size[1] - pad[2]) - y = -10; - if (point[1] < pad[0]) - y = 10; - - if (x || y) { - return [x, y]; - } else { - return null; - } - } + var _nudgeInterval; + var _restoreSelectedIDs = []; + var _wasMidpoint = false; + var _isCancelled = false; + var _activeEntity; + var _startLoc; + var _lastLoc; function startNudge(entity, nudge) { - if (nudgeInterval) window.clearInterval(nudgeInterval); - nudgeInterval = window.setInterval(function() { + if (_nudgeInterval) window.clearInterval(_nudgeInterval); + _nudgeInterval = window.setInterval(function() { context.pan(nudge); doMove(entity, nudge); }, 50); @@ -83,9 +61,9 @@ export function modeDragNode(context) { function stopNudge() { - if (nudgeInterval) { - window.clearInterval(nudgeInterval); - nudgeInterval = null; + if (_nudgeInterval) { + window.clearInterval(_nudgeInterval); + _nudgeInterval = null; } } @@ -106,46 +84,52 @@ export function modeDragNode(context) { function start(entity) { - wasMidpoint = entity.type === 'midpoint'; + _wasMidpoint = entity.type === 'midpoint'; var hasHidden = context.features().hasHiddenConnections(entity, context.graph()); - isCancelled = d3_event.sourceEvent.shiftKey || hasHidden; + _isCancelled = d3_event.sourceEvent.shiftKey || hasHidden; - if (isCancelled) { + if (_isCancelled) { if (hasHidden) { uiFlash().text(t('modes.drag_node.connected_to_hidden'))(); } - return behavior.cancel(); + return drag.cancel(); } - if (wasMidpoint) { + if (_wasMidpoint) { var midpoint = entity; entity = osmNode(); context.perform(actionAddMidpoint(midpoint, entity)); + entity = context.entity(entity.id); // get post-action entity var vertex = context.surface().selectAll('.' + entity.id); - behavior.target(vertex.node(), entity); + drag.target(vertex.node(), entity); } else { context.perform(actionNoop()); } - // activeIDs generate no pointer events. This prevents the node or vertex - // being dragged from trying to connect to itself or its parent element. - activeIDs = _map(context.graph().parentWays(entity), 'id'); - activeIDs.push(entity.id); - setActiveElements(); + _activeEntity = entity; + _startLoc = entity.loc; + + context.surface().selectAll('.' + _activeEntity.id) + .classed('active', true); context.enter(mode); } + // related code + // - `behavior/draw.js` `datum()` function datum() { var event = d3_event && d3_event.sourceEvent; if (!event || event.altKey) { return {}; } else { - return event.target.__data__ || {}; + // When dragging, snap only to touch targets.. + // (this excludes area fills and active drawing elements) + var d = event.target.__data__; + return (d && d.properties && d.properties.target) ? d : {}; } } @@ -153,16 +137,32 @@ export function modeDragNode(context) { function doMove(entity, nudge) { nudge = nudge || [0, 0]; - var currPoint = (d3_event && d3_event.point) || context.projection(lastLoc), - currMouse = vecSub(currPoint, nudge), - loc = context.projection.invert(currMouse), - d = datum(); + var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); + var currMouse = geoVecSubtract(currPoint, nudge); + var loc = context.projection.invert(currMouse); - if (!nudgeInterval) { - if (d.type === 'node' && d.id !== entity.id) { - loc = d.loc; - } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { - loc = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection).loc; + if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap.. + // related code + // - `mode/drag_node.js` `doMode()` + // - `behavior/draw.js` `click()` + // - `behavior/draw_way.js` `move()` + var d = datum(); + var nodeLoc = d && d.properties && d.properties.entity && d.properties.entity.loc; + var nodeGroups = d && d.properties && d.properties.nodes; + + if (nodeLoc) { // snap to node/vertex - a point target with `.loc` + loc = nodeLoc; + + } else if (nodeGroups) { // snap to way - a line target with `.nodes` + var best = Infinity; + for (var i = 0; i < nodeGroups.length; i++) { + var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); + var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, entity.id); + if (choice && choice.distance < best) { + best = choice.distance; + loc = choice.loc; + } + } } } @@ -171,17 +171,79 @@ export function modeDragNode(context) { moveAnnotation(entity) ); - lastLoc = loc; + + // check if this movement causes the geometry to break + var doBlock = invalidGeometry(entity, context.graph()); + context.surface() + .classed('nope', doBlock); + + _lastLoc = loc; + } + + + function invalidGeometry(entity, graph) { + var parents = graph.parentWays(entity); + var i, j, k; + + for (i = 0; i < parents.length; i++) { + var parent = parents[i]; + var nodes = []; + var activeIndex = null; // which multipolygon ring contains node being dragged + + // test any parent multipolygons for valid geometry + var relations = graph.parentRelations(parent); + for (j = 0; j < relations.length; j++) { + if (!relations[j].isMultipolygon()) continue; + + var rings = osmJoinWays(relations[j].members, graph); + + // find active ring and test it for self intersections + for (k = 0; k < rings.length; k++) { + nodes = rings[k].nodes; + if (_find(nodes, function(n) { return n.id === entity.id; })) { + activeIndex = k; + if (geoHasSelfIntersections(nodes, entity.id)) { + return true; + } + } + rings[k].coords = nodes.map(function(n) { return n.loc; }); + } + + // test active ring for intersections with other rings in the multipolygon + for (k = 0; k < rings.length; k++) { + if (k === activeIndex) continue; + + // make sure active ring doesnt cross passive rings + if (geoPathHasIntersections(rings[activeIndex].coords, rings[k].coords)) { + return true; + } + } + } + + + // If we still haven't tested this node's parent way for self-intersections. + // (because it's not a member of a multipolygon), test it now. + if (activeIndex !== null && parent.isClosed()) { + nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); + if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) { + return true; + } + } + + } + + return false; } function move(entity) { - if (isCancelled) return; + if (_isCancelled) return; + d3_event.sourceEvent.stopPropagation(); - lastLoc = context.projection.invert(d3_event.point); + _lastLoc = context.projection.invert(d3_event.point); doMove(entity); - var nudge = edge(d3_event.point, context.map().dimensions()); + var nudge = geoViewportEdge(d3_event.point, context.map().dimensions()); if (nudge) { startNudge(entity, nudge); } else { @@ -191,24 +253,34 @@ export function modeDragNode(context) { function end(entity) { - if (isCancelled) return; + if (_isCancelled) return; var d = datum(); + var nope = (d && d.properties && d.properties.nope) || context.surface().classed('nope'); + var target = d && d.properties && d.properties.entity; // entity to snap to - if (d.type === 'way') { - var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); - context.replace( - actionAddMidpoint({ loc: choice.loc, edge: [d.nodes[choice.index - 1], d.nodes[choice.index]] }, entity), - connectAnnotation(d) + if (nope) { // bounce back + context.perform( + _actionBounceBack(entity.id, _startLoc) ); - } else if (d.type === 'node' && d.id !== entity.id) { + } else if (target && target.type === 'way') { + var choice = geoChooseEdge(context.childNodes(target), context.mouse(), context.projection, entity.id); context.replace( - actionConnect([d.id, entity.id]), - connectAnnotation(d) + actionAddMidpoint({ + loc: choice.loc, + edge: [target.nodes[choice.index - 1], target.nodes[choice.index]] + }, entity), + connectAnnotation(target) ); - } else if (wasMidpoint) { + } else if (target && target.type === 'node') { + context.replace( + actionConnect([target.id, entity.id]), + connectAnnotation(target) + ); + + } else if (_wasMidpoint) { context.replace( actionNoop(), t('operations.add.annotation.vertex') @@ -221,7 +293,7 @@ export function modeDragNode(context) { ); } - var reselection = selectedIDs.filter(function(id) { + var reselection = _restoreSelectedIDs.filter(function(id) { return context.graph().hasEntity(id); }); @@ -233,20 +305,27 @@ export function modeDragNode(context) { } + function _actionBounceBack(nodeID, toLoc) { + var moveNode = actionMoveNode(nodeID, toLoc); + var action = function(graph, t) { + // last time through, pop off the bounceback perform. + // it will then overwrite the initial perform with a moveNode that does nothing + if (t === 1) context.pop(); + return moveNode(graph, t); + }; + action.transitionable = true; + return action; + } + + function cancel() { - behavior.cancel(); + drag.cancel(); context.enter(modeBrowse(context)); } - function setActiveElements() { - context.surface().selectAll(utilEntitySelector(activeIDs)) - .classed('active', true); - } - - - var behavior = behaviorDrag() - .selector('g.node, g.point, g.midpoint') + var drag = behaviorDrag() + .selector('.layer-points-targets .target') .surface(d3_select('#map').node()) .origin(origin) .on('start', start) @@ -260,11 +339,6 @@ export function modeDragNode(context) { context.history() .on('undone.drag-node', cancel); - - context.map() - .on('drawn.drag-node', setActiveElements); - - setActiveElements(); }; @@ -279,7 +353,10 @@ export function modeDragNode(context) { context.map() .on('drawn.drag-node', null); + _activeEntity = null; + context.surface() + .classed('nope', false) .selectAll('.active') .classed('active', false); @@ -287,14 +364,28 @@ export function modeDragNode(context) { }; - mode.selectedIDs = function(_) { - if (!arguments.length) return selectedIDs; - selectedIDs = _; + mode.selectedIDs = function() { + if (!arguments.length) return _activeEntity ? [_activeEntity.id] : []; + // no assign return mode; }; - mode.behavior = behavior; + mode.activeID = function() { + if (!arguments.length) return _activeEntity && _activeEntity.id; + // no assign + return mode; + }; + + + mode.restoreSelectedIDs = function(_) { + if (!arguments.length) return _restoreSelectedIDs; + _restoreSelectedIDs = _; + return mode; + }; + + + mode.behavior = drag; return mode; diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index e5478d339..0890e3592 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -20,8 +20,8 @@ export function modeDrawArea(context, wayId, startGraph) { var addNode = behavior.addNode; behavior.addNode = function(node) { - var length = way.nodes.length, - penultimate = length > 2 ? way.nodes[length - 2] : null; + var length = way.nodes.length; + var penultimate = length > 2 ? way.nodes[length - 2] : null; if (node.id === way.first() || node.id === penultimate) { behavior.finish(); @@ -44,5 +44,10 @@ export function modeDrawArea(context, wayId, startGraph) { }; + mode.activeID = function() { + return (behavior && behavior.activeID()) || []; + }; + + return mode; } diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index 5198fd444..ca567601e 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -12,15 +12,14 @@ export function modeDrawLine(context, wayId, startGraph, affix) { mode.enter = function() { - var way = context.entity(wayId), - index = (affix === 'prefix') ? 0 : undefined, - headId = (affix === 'prefix') ? way.first() : way.last(); + var way = context.entity(wayId); + var index = (affix === 'prefix') ? 0 : undefined; + var headId = (affix === 'prefix') ? way.first() : way.last(); behavior = behaviorDrawWay(context, wayId, index, mode, startGraph) .tail(t('modes.draw_line.tail')); var addNode = behavior.addNode; - behavior.addNode = function(node) { if (node.id === headId) { behavior.finish(); @@ -43,5 +42,9 @@ export function modeDrawLine(context, wayId, startGraph, affix) { }; + mode.activeID = function() { + return (behavior && behavior.activeID()) || []; + }; + return mode; } diff --git a/modules/modes/move.js b/modules/modes/move.js index a6571d332..e91439b1b 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -8,6 +8,7 @@ import { t } from '../util/locale'; import { actionMove } from '../actions'; import { behaviorEdit } from '../behavior'; +import { geoViewportEdge } from '../geo'; import { modeBrowse, @@ -30,23 +31,24 @@ export function modeMove(context, entityIDs, baseGraph) { button: 'browse' }; - var keybinding = d3_keybinding('move'), - behaviors = [ - behaviorEdit(context), - operationCircularize(entityIDs, context).behavior, - operationDelete(entityIDs, context).behavior, - operationOrthogonalize(entityIDs, context).behavior, - operationReflectLong(entityIDs, context).behavior, - operationReflectShort(entityIDs, context).behavior, - operationRotate(entityIDs, context).behavior - ], - annotation = entityIDs.length === 1 ? - t('operations.move.annotation.' + context.geometry(entityIDs[0])) : - t('operations.move.annotation.multiple'), - prevGraph, - cache, - origin, - nudgeInterval; + var keybinding = d3_keybinding('move'); + var behaviors = [ + behaviorEdit(context), + operationCircularize(entityIDs, context).behavior, + operationDelete(entityIDs, context).behavior, + operationOrthogonalize(entityIDs, context).behavior, + operationReflectLong(entityIDs, context).behavior, + operationReflectShort(entityIDs, context).behavior, + operationRotate(entityIDs, context).behavior + ]; + var annotation = entityIDs.length === 1 ? + t('operations.move.annotation.' + context.geometry(entityIDs[0])) : + t('operations.move.annotation.multiple'); + + var _prevGraph; + var _cache; + var _origin; + var _nudgeInterval; function vecSub(a, b) { @@ -54,52 +56,30 @@ export function modeMove(context, entityIDs, baseGraph) { } - function edge(point, size) { - var pad = [80, 20, 50, 20], // top, right, bottom, left - x = 0, - y = 0; - - if (point[0] > size[0] - pad[1]) - x = -10; - if (point[0] < pad[3]) - x = 10; - if (point[1] > size[1] - pad[2]) - y = -10; - if (point[1] < pad[0]) - y = 10; - - if (x || y) { - return [x, y]; - } else { - return null; - } - } - - function doMove(nudge) { nudge = nudge || [0, 0]; var fn; - if (prevGraph !== context.graph()) { - cache = {}; - origin = context.map().mouseCoordinates(); + if (_prevGraph !== context.graph()) { + _cache = {}; + _origin = context.map().mouseCoordinates(); fn = context.perform; } else { fn = context.overwrite; } - var currMouse = context.mouse(), - origMouse = context.projection(origin), - delta = vecSub(vecSub(currMouse, origMouse), nudge); + var currMouse = context.mouse(); + var origMouse = context.projection(_origin); + var delta = vecSub(vecSub(currMouse, origMouse), nudge); - fn(actionMove(entityIDs, delta, context.projection, cache), annotation); - prevGraph = context.graph(); + fn(actionMove(entityIDs, delta, context.projection, _cache), annotation); + _prevGraph = context.graph(); } function startNudge(nudge) { - if (nudgeInterval) window.clearInterval(nudgeInterval); - nudgeInterval = window.setInterval(function() { + if (_nudgeInterval) window.clearInterval(_nudgeInterval); + _nudgeInterval = window.setInterval(function() { context.pan(nudge); doMove(nudge); }, 50); @@ -107,16 +87,16 @@ export function modeMove(context, entityIDs, baseGraph) { function stopNudge() { - if (nudgeInterval) { - window.clearInterval(nudgeInterval); - nudgeInterval = null; + if (_nudgeInterval) { + window.clearInterval(_nudgeInterval); + _nudgeInterval = null; } } function move() { doMove(); - var nudge = edge(context.mouse(), context.map().dimensions()); + var nudge = geoViewportEdge(context.mouse(), context.map().dimensions()); if (nudge) { startNudge(nudge); } else { @@ -150,9 +130,9 @@ export function modeMove(context, entityIDs, baseGraph) { mode.enter = function() { - origin = context.map().mouseCoordinates(); - prevGraph = null; - cache = {}; + _origin = context.map().mouseCoordinates(); + _prevGraph = null; + _cache = {}; behaviors.forEach(function(behavior) { context.install(behavior); @@ -192,5 +172,12 @@ export function modeMove(context, entityIDs, baseGraph) { }; + mode.selectedIDs = function() { + if (!arguments.length) return entityIDs; + // no assign + return mode; + }; + + return mode; } diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index 4efd701dd..4addaed88 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -13,7 +13,7 @@ import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; import { actionRotate } from '../actions'; import { behaviorEdit } from '../behavior'; -import { geoInterp } from '../geo'; +import { geoVecInterp } from '../geo'; import { modeBrowse, @@ -38,66 +38,67 @@ export function modeRotate(context, entityIDs) { button: 'browse' }; - var keybinding = d3_keybinding('rotate'), - behaviors = [ - behaviorEdit(context), - operationCircularize(entityIDs, context).behavior, - operationDelete(entityIDs, context).behavior, - operationMove(entityIDs, context).behavior, - operationOrthogonalize(entityIDs, context).behavior, - operationReflectLong(entityIDs, context).behavior, - operationReflectShort(entityIDs, context).behavior - ], - annotation = entityIDs.length === 1 ? - t('operations.rotate.annotation.' + context.geometry(entityIDs[0])) : - t('operations.rotate.annotation.multiple'), - prevGraph, - prevAngle, - prevTransform, - pivot; + var keybinding = d3_keybinding('rotate'); + var behaviors = [ + behaviorEdit(context), + operationCircularize(entityIDs, context).behavior, + operationDelete(entityIDs, context).behavior, + operationMove(entityIDs, context).behavior, + operationOrthogonalize(entityIDs, context).behavior, + operationReflectLong(entityIDs, context).behavior, + operationReflectShort(entityIDs, context).behavior + ]; + var annotation = entityIDs.length === 1 ? + t('operations.rotate.annotation.' + context.geometry(entityIDs[0])) : + t('operations.rotate.annotation.multiple'); + + var _prevGraph; + var _prevAngle; + var _prevTransform; + var _pivot; function doRotate() { var fn; - if (context.graph() !== prevGraph) { + if (context.graph() !== _prevGraph) { fn = context.perform; } else { fn = context.replace; } - // projection changed, recalculate pivot + // projection changed, recalculate _pivot var projection = context.projection; var currTransform = projection.transform(); - if (!prevTransform || - currTransform.k !== prevTransform.k || - currTransform.x !== prevTransform.x || - currTransform.y !== prevTransform.y) { + if (!_prevTransform || + currTransform.k !== _prevTransform.k || + currTransform.x !== _prevTransform.x || + currTransform.y !== _prevTransform.y) { - var nodes = utilGetAllNodes(entityIDs, context.graph()), - points = nodes.map(function(n) { return projection(n.loc); }); + var nodes = utilGetAllNodes(entityIDs, context.graph()); + var points = nodes.map(function(n) { return projection(n.loc); }); if (points.length === 1) { // degenerate case - pivot = points[0]; + _pivot = points[0]; } else if (points.length === 2) { - pivot = geoInterp(points[0], points[1], 0.5); + _pivot = geoVecInterp(points[0], points[1], 0.5); } else { - pivot = d3_polygonCentroid(d3_polygonHull(points)); + _pivot = d3_polygonCentroid(d3_polygonHull(points)); } - prevAngle = undefined; + _prevAngle = undefined; } - var currMouse = context.mouse(), - currAngle = Math.atan2(currMouse[1] - pivot[1], currMouse[0] - pivot[0]); + var currMouse = context.mouse(); + var currAngle = Math.atan2(currMouse[1] - _pivot[1], currMouse[0] - _pivot[0]); - if (typeof prevAngle === 'undefined') prevAngle = currAngle; - var delta = currAngle - prevAngle; + if (typeof _prevAngle === 'undefined') _prevAngle = currAngle; + var delta = currAngle - _prevAngle; - fn(actionRotate(entityIDs, pivot, delta, projection), annotation); + fn(actionRotate(entityIDs, _pivot, delta, projection), annotation); - prevTransform = currTransform; - prevAngle = currAngle; - prevGraph = context.graph(); + _prevTransform = currTransform; + _prevAngle = currAngle; + _prevGraph = context.graph(); } @@ -155,5 +156,12 @@ export function modeRotate(context, entityIDs) { }; + mode.selectedIDs = function() { + if (!arguments.length) return entityIDs; + // no assign + return mode; + }; + + return mode; } diff --git a/modules/modes/select.js b/modules/modes/select.js index 281e77709..1c87a96ed 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -63,7 +63,7 @@ export function modeSelect(context, selectedIDs) { behaviorHover(context), behaviorSelect(context), behaviorLasso(context), - modeDragNode(context).selectedIDs(selectedIDs).behavior + modeDragNode(context).restoreSelectedIDs(selectedIDs).behavior ], inspector, editMenu, @@ -245,13 +245,16 @@ export function modeSelect(context, selectedIDs) { function dblclick() { - var target = d3_select(d3_event.target), - datum = target.datum(); + var target = d3_select(d3_event.target); - if (datum instanceof osmWay && !target.classed('fill')) { - var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection), - prev = datum.nodes[choice.index - 1], - next = datum.nodes[choice.index]; + var datum = target.datum(); + var entity = datum && datum.id && context.hasEntity(datum.id); + if (entity) datum = entity; + + if (datum instanceof osmWay && target.classed('target')) { + var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection); + var prev = datum.nodes[choice.index - 1]; + var next = datum.nodes[choice.index]; context.perform( actionAddMidpoint({loc: choice.loc, edge: [prev, next]}, osmNode()), diff --git a/modules/osm/node.js b/modules/osm/node.js index 59d299dbb..5083caada 100644 --- a/modules/osm/node.js +++ b/modules/osm/node.js @@ -3,7 +3,7 @@ import _map from 'lodash-es/map'; import _some from 'lodash-es/some'; import { osmEntity } from './entity'; -import { geoExtent } from '../geo'; +import { geoAngle, geoExtent } from '../geo'; export function osmNode() { @@ -49,6 +49,87 @@ _extend(osmNode.prototype, { }, + // Inspect tags and geometry to determine which direction(s) this node/vertex points + directions: function(resolver, projection) { + var val; + var i; + + // which tag to use? + if (this.isHighwayIntersection(resolver) && (this.tags.stop || '').toLowerCase() === 'all') { + // all-way stop tag on a highway intersection + val = 'all'; + } else { + // generic direction tag + val = (this.tags.direction || '').toLowerCase(); + + // better suffix-style direction tag + var re = /:direction$/i; + var keys = Object.keys(this.tags); + for (i = 0; i < keys.length; i++) { + if (re.test(keys[i])) { + val = this.tags[keys[i]].toLowerCase(); + break; + } + } + } + + // swap cardinal for numeric directions + var cardinal = { + north: 0, n: 0, + northnortheast: 22, nne: 22, + northeast: 45, ne: 45, + eastnortheast: 67, ene: 67, + east: 90, e: 90, + eastsoutheast: 112, ese: 112, + southeast: 135, se: 135, + southsoutheast: 157, sse: 157, + south: 180, s: 180, + southsouthwest: 202, ssw: 202, + southwest: 225, sw: 225, + westsouthwest: 247, wsw: 247, + west: 270, w: 270, + westnorthwest: 292, wnw: 292, + northwest: 315, nw: 315, + northnorthwest: 337, nnw: 337 + }; + if (cardinal[val] !== undefined) { + val = cardinal[val]; + } + + // if direction is numeric, return early + if (val !== '' && !isNaN(+val)) { + return [(+val)]; + } + + var lookBackward = + (this.tags['traffic_sign:backward'] || val === 'backward' || val === 'both' || val === 'all'); + var lookForward = + (this.tags['traffic_sign:forward'] || val === 'forward' || val === 'both' || val === 'all'); + + if (!lookForward && !lookBackward) return []; + + var nodeIds = {}; + resolver.parentWays(this).forEach(function(parent) { + var nodes = parent.nodes; + for (i = 0; i < nodes.length; i++) { + if (nodes[i] === this.id) { // match current entity + if (lookForward && i > 0) { + nodeIds[nodes[i - 1]] = true; // look back to prev node + } + if (lookBackward && i < nodes.length - 1) { + nodeIds[nodes[i + 1]] = true; // look ahead to next node + } + } + } + }, this); + + return Object.keys(nodeIds).map(function(nodeId) { + // +90 because geoAngle returns angle from X axis, not Y (north) + return (geoAngle(this, resolver.entity(nodeId), projection) * (180 / Math.PI)) + 90; + }, this); + }, + + isEndpoint: function(resolver) { return resolver.transient(this, 'isEndpoint', function() { var id = this.id; diff --git a/modules/osm/way.js b/modules/osm/way.js index 76918a52e..14e4e410c 100644 --- a/modules/osm/way.js +++ b/modules/osm/way.js @@ -4,7 +4,7 @@ import _uniq from 'lodash-es/uniq'; import { geoArea as d3_geoArea } from 'd3-geo'; -import { geoExtent, geoCross } from '../geo'; +import { geoExtent, geoVecCross } from '../geo'; import { osmEntity } from './entity'; import { osmLanes } from './lanes'; import { osmOneWayTags } from './tags'; @@ -133,15 +133,16 @@ _extend(osmWay.prototype, { isConvex: function(resolver) { if (!this.isClosed() || this.isDegenerate()) return null; - var nodes = _uniq(resolver.childNodes(this)), - coords = _map(nodes, 'loc'), - curr = 0, prev = 0; + var nodes = _uniq(resolver.childNodes(this)); + var coords = _map(nodes, 'loc'); + var curr = 0; + var prev = 0; for (var i = 0; i < coords.length; i++) { - var o = coords[(i+1) % coords.length], - a = coords[i], - b = coords[(i+2) % coords.length], - res = geoCross(o, a, b); + var o = coords[(i+1) % coords.length]; + var a = coords[i]; + var b = coords[(i+2) % coords.length]; + var res = geoVecCross(a, b, o); curr = (res > 0) ? 1 : (res < 0) ? -1 : 0; if (curr === 0) { diff --git a/modules/renderer/map.js b/modules/renderer/map.js index e53297ff4..a4ebac8a4 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -183,33 +183,55 @@ export function rendererMap(context) { if (map.editable() && !transformed) { var hover = d3_event.target.__data__; surface.selectAll('.data-layer-osm') - .call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom()); - dispatch.call('drawn', this, {full: false}); + .call(drawVertices.drawHover, context.graph(), hover, map.extent()); + dispatch.call('drawn', this, { full: false }); } }) .on('mouseout.vertices', function() { if (map.editable() && !transformed) { var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; surface.selectAll('.data-layer-osm') - .call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom()); - dispatch.call('drawn', this, {full: false}); + .call(drawVertices.drawHover, context.graph(), hover, map.extent()); + dispatch.call('drawn', this, { full: false }); } }); supersurface .call(context.background()); - context.on('enter.map', function() { + context.on('enter.map', function() { if (map.editable() && !transformed) { - var all = context.intersects(map.extent()), - filter = utilFunctor(true), - graph = context.graph(); - all = context.features().filter(all, graph); + // redraw immediately any objects affected by a change in selectedIDs. + var graph = context.graph(); + var selectedAndParents = {}; + context.selectedIDs().forEach(function(id) { + var entity = graph.hasEntity(id); + if (entity) { + selectedAndParents[entity.id] = entity; + if (entity.type === 'node') { + graph.parentWays(entity).forEach(function(parent) { + selectedAndParents[parent.id] = parent; + }); + } + } + }); + var data = _values(selectedAndParents); + var filter = function(d) { return d.id in selectedAndParents; }; + + data = context.features().filter(data, graph); + surface.selectAll('.data-layer-osm') - .call(drawVertices, graph, all, filter, map.extent(), map.zoom()) - .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); - dispatch.call('drawn', this, {full: false}); + .call(drawVertices.drawSelected, graph, map.extent()) + .call(drawLines, graph, data, filter) + .call(drawAreas, graph, data, filter) + .call(drawMidpoints, graph, data, filter, map.trimmedExtent()); + + dispatch.call('drawn', this, { full: false }); + + + // redraw everything else later + scheduleRedraw(); } }); @@ -265,10 +287,13 @@ export function rendererMap(context) { function drawVector(difference, extent) { - var graph = context.graph(), - features = context.features(), - all = context.intersects(map.extent()), - data, filter; + var mode = context.mode(); + var graph = context.graph(); + var features = context.features(); + var all = context.intersects(map.extent()); + var fullRedraw = false; + var data; + var filter; if (difference) { var complete = difference.complete(map.extent()); @@ -290,18 +315,26 @@ export function rendererMap(context) { } else { data = all; + fullRedraw = true; filter = utilFunctor(true); } } data = features.filter(data, graph); + if (mode && mode.id === 'select') { + // update selected vertices - the user might have just double-clicked a way, + // creating a new vertex, triggering a partial redraw without a mode change + surface.selectAll('.data-layer-osm') + .call(drawVertices.drawSelected, graph, map.extent()); + } + surface.selectAll('.data-layer-osm') - .call(drawVertices, graph, data, filter, map.extent(), map.zoom()) + .call(drawVertices, graph, data, filter, map.extent(), fullRedraw) .call(drawLines, graph, data, filter) .call(drawAreas, graph, data, filter) .call(drawMidpoints, graph, data, filter, map.trimmedExtent()) - .call(drawLabels, graph, data, filter, dimensions, !difference && !extent) + .call(drawLabels, graph, data, filter, dimensions, fullRedraw) .call(drawPoints, graph, data, filter); dispatch.call('drawn', this, {full: true}); diff --git a/modules/renderer/tile_layer.js b/modules/renderer/tile_layer.js index ceb846117..b0fb42aa3 100644 --- a/modules/renderer/tile_layer.js +++ b/modules/renderer/tile_layer.js @@ -2,28 +2,29 @@ import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; -import { geoEuclideanDistance } from '../geo'; +import { geoScaleToZoom, geoVecLength } from '../geo'; import { utilPrefixCSSProperty } from '../util'; export function rendererTileLayer(context) { - var tileSize = 256, - geotile = d3_geoTile(), - projection, - cache = {}, - tileOrigin, - z, - transformProp = utilPrefixCSSProperty('Transform'), - source; + var tileSize = 256; + var transformProp = utilPrefixCSSProperty('Transform'); + var geotile = d3_geoTile(); + + var _projection; + var _cache = {}; + var _tileOrigin; + var _zoom; + var _source; // blacklist overlay tiles around Null Island.. function nearNullIsland(x, y, z) { if (z >= 7) { - var center = Math.pow(2, z - 1), - width = Math.pow(2, z - 6), - min = center - (width / 2), - max = center + (width / 2) - 1; + var center = Math.pow(2, z - 1); + var width = Math.pow(2, z - 6); + var min = center - (width / 2); + var max = center + (width / 2) - 1; return x >= min && x <= max && y >= min && y <= max; } return false; @@ -31,8 +32,8 @@ export function rendererTileLayer(context) { function tileSizeAtZoom(d, z) { - var epsilon = 0.002; - return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + epsilon; + var EPSILON = 0.002; + return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + EPSILON; } @@ -49,7 +50,7 @@ export function rendererTileLayer(context) { function lookUp(d) { for (var up = -1; up > -d[2]; up--) { var tile = atZoom(d, up); - if (cache[source.url(tile)] !== false) { + if (_cache[_source.url(tile)] !== false) { return tile; } } @@ -57,7 +58,8 @@ export function rendererTileLayer(context) { function uniqueBy(a, n) { - var o = [], seen = {}; + var o = []; + var seen = {}; for (var i = 0; i < a.length; i++) { if (seen[a[i][n]] === undefined) { o.push(a[i]); @@ -69,37 +71,37 @@ export function rendererTileLayer(context) { function addSource(d) { - d.push(source.url(d)); + d.push(_source.url(d)); return d; } // Update tiles based on current state of `projection`. function background(selection) { - z = Math.max(Math.log(projection.scale() * 2 * Math.PI) / Math.log(2) - 8, 0); + _zoom = geoScaleToZoom(_projection.scale(), tileSize); var pixelOffset; - if (source) { + if (_source) { pixelOffset = [ - source.offset()[0] * Math.pow(2, z), - source.offset()[1] * Math.pow(2, z) + _source.offset()[0] * Math.pow(2, _zoom), + _source.offset()[1] * Math.pow(2, _zoom) ]; } else { pixelOffset = [0, 0]; } var translate = [ - projection.translate()[0] + pixelOffset[0], - projection.translate()[1] + pixelOffset[1] + _projection.translate()[0] + pixelOffset[0], + _projection.translate()[1] + pixelOffset[1] ]; geotile - .scale(projection.scale() * 2 * Math.PI) + .scale(_projection.scale() * 2 * Math.PI) .translate(translate); - tileOrigin = [ - projection.scale() * Math.PI - translate[0], - projection.scale() * Math.PI - translate[1] + _tileOrigin = [ + _projection.scale() * Math.PI - translate[0], + _projection.scale() * Math.PI - translate[1] ]; render(selection); @@ -107,36 +109,36 @@ export function rendererTileLayer(context) { // Derive the tiles onscreen, remove those offscreen and position them. - // Important that this part not depend on `projection` because it's + // Important that this part not depend on `_projection` because it's // rentered when tiles load/error (see #644). function render(selection) { - if (!source) return; + if (!_source) return; var requests = []; - var showDebug = context.getDebug('tile') && !source.overlay; + var showDebug = context.getDebug('tile') && !_source.overlay; - if (source.validZoom(z)) { + if (_source.validZoom(_zoom)) { geotile().forEach(function(d) { addSource(d); if (d[3] === '') return; if (typeof d[3] !== 'string') return; // Workaround for #2295 requests.push(d); - if (cache[d[3]] === false && lookUp(d)) { + if (_cache[d[3]] === false && lookUp(d)) { requests.push(addSource(lookUp(d))); } }); requests = uniqueBy(requests, 3).filter(function(r) { - if (!!source.overlay && nearNullIsland(r[0], r[1], r[2])) { + if (!!_source.overlay && nearNullIsland(r[0], r[1], r[2])) { return false; } // don't re-request tiles which have failed in the past - return cache[r[3]] !== false; + return _cache[r[3]] !== false; }); } function load(d) { - cache[d[3]] = true; + _cache[d[3]] = true; d3_select(this) .on('error', null) .on('load', null) @@ -145,7 +147,7 @@ export function rendererTileLayer(context) { } function error(d) { - cache[d[3]] = false; + _cache[d[3]] = false; d3_select(this) .on('error', null) .on('load', null) @@ -154,19 +156,19 @@ export function rendererTileLayer(context) { } function imageTransform(d) { - var _ts = tileSize * Math.pow(2, z - d[2]); - var scale = tileSizeAtZoom(d, z); + var ts = tileSize * Math.pow(2, _zoom - d[2]); + var scale = tileSizeAtZoom(d, _zoom); return 'translate(' + - ((d[0] * _ts) - tileOrigin[0]) + 'px,' + - ((d[1] * _ts) - tileOrigin[1]) + 'px) ' + + ((d[0] * ts) - _tileOrigin[0]) + 'px,' + + ((d[1] * ts) - _tileOrigin[1]) + 'px) ' + 'scale(' + scale + ',' + scale + ')'; } function tileCenter(d) { - var _ts = tileSize * Math.pow(2, z - d[2]); + var ts = tileSize * Math.pow(2, _zoom - d[2]); return [ - ((d[0] * _ts) - tileOrigin[0] + (_ts / 2)), - ((d[1] * _ts) - tileOrigin[1] + (_ts / 2)) + ((d[0] * ts) - _tileOrigin[0] + (ts / 2)), + ((d[1] * ts) - _tileOrigin[1] + (ts / 2)) ]; } @@ -178,14 +180,14 @@ export function rendererTileLayer(context) { // Pick a representative tile near the center of the viewport // (This is useful for sampling the imagery vintage) - var dims = geotile.size(), - mapCenter = [dims[0] / 2, dims[1] / 2], - minDist = Math.max(dims[0], dims[1]), - nearCenter; + var dims = geotile.size(); + var mapCenter = [dims[0] / 2, dims[1] / 2]; + var minDist = Math.max(dims[0], dims[1]); + var nearCenter; requests.forEach(function(d) { var c = tileCenter(d); - var dist = geoEuclideanDistance(c, mapCenter); + var dist = geoVecLength(c, mapCenter); if (dist < minDist) { minDist = dist; nearCenter = d; @@ -255,8 +257,8 @@ export function rendererTileLayer(context) { .selectAll('.tile-label-debug-vintage') .each(function(d) { var span = d3_select(this); - var center = context.projection.invert(tileCenter(d)); - source.getMetadata(center, d, function(err, result) { + var center = context._projection.invert(tileCenter(d)); + _source.getMetadata(center, d, function(err, result) { span.text((result && result.vintage && result.vintage.range) || t('info_panels.background.vintage') + ': ' + t('info_panels.background.unknown') ); @@ -268,8 +270,8 @@ export function rendererTileLayer(context) { background.projection = function(_) { - if (!arguments.length) return projection; - projection = _; + if (!arguments.length) return _projection; + _projection = _; return background; }; @@ -282,10 +284,10 @@ export function rendererTileLayer(context) { background.source = function(_) { - if (!arguments.length) return source; - source = _; - cache = {}; - geotile.scaleExtent(source.scaleExtent); + if (!arguments.length) return _source; + _source = _; + _cache = {}; + geotile.scaleExtent(_source.scaleExtent); return background; }; diff --git a/modules/services/osm.js b/modules/services/osm.js index cf33659a3..af96685a3 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -500,8 +500,8 @@ export default { } // update blacklists - var elements = xml.getElementsByTagName('blacklist'), - regexes = []; + var elements = xml.getElementsByTagName('blacklist'); + var regexes = []; for (var i = 0; i < elements.length; i++) { var regex = elements[i].getAttribute('regex'); // needs unencode? if (regex) { @@ -516,8 +516,8 @@ export default { if (rateLimitError) { callback(rateLimitError, 'rateLimited'); } else { - var apiStatus = xml.getElementsByTagName('status'), - val = apiStatus[0].getAttribute('api'); + var apiStatus = xml.getElementsByTagName('status'); + var val = apiStatus[0].getAttribute('api'); callback(undefined, val); } @@ -544,14 +544,14 @@ export default { loadTiles: function(projection, dimensions, callback) { if (off) return; - var that = this, - s = projection.scale() * 2 * Math.PI, - z = Math.max(Math.log(s) / Math.log(2) - 8, 0), - ts = 256 * Math.pow(2, z - tileZoom), - origin = [ - s / 2 - projection.translate()[0], - s / 2 - projection.translate()[1] - ]; + var that = this; + var s = projection.scale() * 2 * Math.PI; + var z = Math.max(Math.log(s) / Math.log(2) - 8, 0); + var ts = 256 * Math.pow(2, z - tileZoom); + var origin = [ + s / 2 - projection.translate()[0], + s / 2 - projection.translate()[1] + ]; var tiles = d3_geoTile() .scaleExtent([tileZoom, tileZoom]) @@ -559,8 +559,8 @@ export default { .size(dimensions) .translate(projection.translate())() .map(function(tile) { - var x = tile[0] * ts - origin[0], - y = tile[1] * ts - origin[1]; + var x = tile[0] * ts - origin[0]; + var y = tile[1] * ts - origin[1]; return { id: tile.toString(), diff --git a/modules/svg/areas.js b/modules/svg/areas.js index 0f219738e..8826ddcad 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -4,7 +4,7 @@ import _values from 'lodash-es/values'; import { bisector as d3_bisector } from 'd3-array'; import { osmEntity, osmIsSimpleMultipolygonOuterMember } from '../osm'; -import { svgPath, svgTagClasses } from './index'; +import { svgPath, svgSegmentWay, svgTagClasses } from './index'; export function svgAreas(projection, context) { @@ -41,7 +41,59 @@ export function svgAreas(projection, context) { } - return function drawAreas(selection, graph, entities, filter) { + function drawTargets(selection, graph, entities, filter) { + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var getPath = svgPath(projection).geojson; + var activeID = context.activeID(); + + // The targets and nopes will be MultiLineString sub-segments of the ways + var data = { targets: [], nopes: [] }; + + entities.forEach(function(way) { + var features = svgSegmentWay(way, graph, activeID); + data.targets.push.apply(data.targets, features.passive); + data.nopes.push.apply(data.nopes, features.active); + }); + + + // Targets allow hover and vertex snapping + var targets = selection.selectAll('.area.target-allowed') + .filter(function(d) { return filter(d.properties.entity); }) + .data(data.targets, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('path') + .merge(targets) + .attr('d', getPath) + .attr('class', function(d) { return 'way area target target-allowed ' + targetClass + d.id; }); + + + // NOPE + var nopes = selection.selectAll('.area.target-nope') + .filter(function(d) { return filter(d.properties.entity); }) + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('path') + .merge(nopes) + .attr('d', getPath) + .attr('class', function(d) { return 'way area target target-nope ' + nopeClass + d.id; }); + } + + + + function drawAreas(selection, graph, entities, filter) { var path = svgPath(projection, graph, true), areas = {}, multipolygon; @@ -99,7 +151,7 @@ export function svgAreas(projection, context) { .attr('d', path); - var layer = selection.selectAll('.layer-areas'); + var layer = selection.selectAll('.layer-areas .layer-areas-areas'); var areagroup = layer .selectAll('g.areagroup') @@ -145,5 +197,12 @@ export function svgAreas(projection, context) { }) .call(svgTagClasses()) .attr('d', path); - }; + + + // touch targets + selection.selectAll('.layer-areas .layer-areas-targets') + .call(drawTargets, graph, data.stroke, filter); + } + + return drawAreas; } diff --git a/modules/svg/debug.js b/modules/svg/debug.js index 9e1c82cb0..71f007417 100644 --- a/modules/svg/debug.js +++ b/modules/svg/debug.js @@ -1,12 +1,8 @@ -import { geoPath as d3_geoPath } from 'd3-geo'; import { select as d3_select } from 'd3-selection'; import { geoPolygonIntersectsPolygon } from '../geo'; -import { - data, - dataImperial, - dataDriveLeft -} from '../../data'; +import { data, dataImperial, dataDriveLeft } from '../../data'; +import { svgPath } from './index'; export function svgDebug(projection, context) { @@ -21,13 +17,12 @@ export function svgDebug(projection, context) { } function drawDebug(selection) { - var showsTile = context.getDebug('tile'), - showsCollision = context.getDebug('collision'), - showsImagery = context.getDebug('imagery'), - showsImperial = context.getDebug('imperial'), - showsDriveLeft = context.getDebug('driveLeft'), - path = d3_geoPath(projection); - + var showsTile = context.getDebug('tile'); + var showsCollision = context.getDebug('collision'); + var showsImagery = context.getDebug('imagery'); + var showsImperial = context.getDebug('imperial'); + var showsDriveLeft = context.getDebug('driveLeft'); + var showsTouchTargets = context.getDebug('target'); var debugData = []; if (showsTile) { @@ -45,6 +40,9 @@ export function svgDebug(projection, context) { if (showsDriveLeft) { debugData.push({ class: 'green', label: 'driveLeft' }); } + if (showsTouchTargets) { + debugData.push({ class: 'pink', label: 'touchTargets' }); + } var legend = d3_select('#content') @@ -84,14 +82,14 @@ export function svgDebug(projection, context) { .merge(layer); - var extent = context.map().extent(), - dataImagery = data.imagery || [], - availableImagery = showsImagery && multipolygons(dataImagery.filter(function(source) { - if (!source.polygon) return false; - return source.polygon.some(function(polygon) { - return geoPolygonIntersectsPolygon(polygon, extent, true); - }); - })); + var extent = context.map().extent(); + var dataImagery = data.imagery || []; + var availableImagery = showsImagery && multipolygons(dataImagery.filter(function(source) { + if (!source.polygon) return false; + return source.polygon.some(function(polygon) { + return geoPolygonIntersectsPolygon(polygon, extent, true); + }); + })); var imagery = layer.selectAll('path.debug-imagery') .data(showsImagery ? availableImagery : []); @@ -130,7 +128,7 @@ export function svgDebug(projection, context) { // update layer.selectAll('path') - .attr('d', path); + .attr('d', svgPath(projection).geojson); } @@ -142,7 +140,8 @@ export function svgDebug(projection, context) { context.getDebug('collision') || context.getDebug('imagery') || context.getDebug('imperial') || - context.getDebug('driveLeft'); + context.getDebug('driveLeft') || + context.getDebug('target'); } else { return this; } diff --git a/modules/svg/defs.js b/modules/svg/defs.js index 93d9663a9..65f37a7ba 100644 --- a/modules/svg/defs.js +++ b/modules/svg/defs.js @@ -26,24 +26,61 @@ export function svgDefs(context) { return function drawDefs(selection) { var defs = selection.append('defs'); - // marker - defs.append('marker') + // markers + defs + .append('marker') .attr('id', 'oneway-marker') - .attr('viewBox', '0 0 10 10') - .attr('refY', 2.5) + .attr('viewBox', '0 0 10 5') .attr('refX', 5) + .attr('refY', 2.5) .attr('markerWidth', 2) .attr('markerHeight', 2) .attr('markerUnits', 'strokeWidth') .attr('orient', 'auto') - .append('path') .attr('class', 'oneway') - .attr('d', 'M 5 3 L 0 3 L 0 2 L 5 2 L 5 0 L 10 2.5 L 5 5 z') + .attr('d', 'M 5,3 L 0,3 L 0,2 L 5,2 L 5,0 L 10,2.5 L 5,5 z') .attr('stroke', 'none') .attr('fill', '#000') .attr('opacity', '0.75'); + defs + .append('marker') + .attr('id', 'viewfield-marker') + .attr('viewBox', '0 0 16 16') + .attr('refX', 8) + .attr('refY', 16) + .attr('markerWidth', 4) + .attr('markerHeight', 4) + .attr('markerUnits', 'strokeWidth') + .attr('orient', 'auto') + .append('path') + .attr('class', 'viewfield') + .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z') + .attr('fill', '#333') + .attr('fill-opacity', '0.75') + .attr('stroke', '#fff') + .attr('stroke-width', '0.5px') + .attr('stroke-opacity', '0.75'); + + defs + .append('marker') + .attr('id', 'viewfield-marker-wireframe') + .attr('viewBox', '0 0 16 16') + .attr('refX', 8) + .attr('refY', 16) + .attr('markerWidth', 4) + .attr('markerHeight', 4) + .attr('markerUnits', 'strokeWidth') + .attr('orient', 'auto') + .append('path') + .attr('class', 'viewfield') + .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z') + .attr('fill', 'none') + .attr('stroke', '#fff') + .attr('stroke-width', '0.5px') + .attr('stroke-opacity', '0.75'); + // patterns var patterns = defs.selectAll('pattern') .data([ @@ -59,23 +96,21 @@ export function svgDefs(context) { ]) .enter() .append('pattern') - .attr('id', function (d) { - return 'pattern-' + d[0]; - }) + .attr('id', function (d) { return 'pattern-' + d[0]; }) .attr('width', 32) .attr('height', 32) .attr('patternUnits', 'userSpaceOnUse'); - patterns.append('rect') + patterns + .append('rect') .attr('x', 0) .attr('y', 0) .attr('width', 32) .attr('height', 32) - .attr('class', function (d) { - return 'pattern-color-' + d[0]; - }); + .attr('class', function (d) { return 'pattern-color-' + d[0]; }); - patterns.append('image') + patterns + .append('image') .attr('x', 0) .attr('y', 0) .attr('width', 32) @@ -85,29 +120,20 @@ export function svgDefs(context) { }); // clip paths - defs.selectAll() + defs.selectAll('clipPath') .data([12, 18, 20, 32, 45]) .enter() .append('clipPath') - .attr('id', function (d) { - return 'clip-square-' + d; - }) + .attr('id', function (d) { return 'clip-square-' + d; }) .append('rect') .attr('x', 0) .attr('y', 0) - .attr('width', function (d) { - return d; - }) - .attr('height', function (d) { - return d; - }); + .attr('width', function (d) { return d; }) + .attr('height', function (d) { return d; }); - defs.call(SVGSpriteDefinition( - 'iD-sprite', - context.imagePath('iD-sprite.svg'))); - - defs.call(SVGSpriteDefinition( - 'maki-sprite', - context.imagePath('maki-sprite.svg'))); + // symbol spritesheets + defs + .call(SVGSpriteDefinition('iD-sprite', context.imagePath('iD-sprite.svg'))) + .call(SVGSpriteDefinition('maki-sprite', context.imagePath('maki-sprite.svg'))); }; } diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js new file mode 100644 index 000000000..3ca51ea8d --- /dev/null +++ b/modules/svg/helpers.js @@ -0,0 +1,286 @@ +import _extend from 'lodash-es/extend'; + +import { + geoIdentity as d3_geoIdentity, + geoPath as d3_geoPath, + geoStream as d3_geoStream +} from 'd3-geo'; + +import { geoVecLength } from '../geo'; + + +// Touch targets control which other vertices we can drag a vertex onto. +// +// - the activeID - nope +// - 1 away (adjacent) to the activeID - yes (vertices will be merged) +// - 2 away from the activeID - nope (would create a self intersecting segment) +// - all others on a linear way - yes +// - all others on a closed way - nope (would create a self intersecting polygon) +// +// returns +// 0 = active vertex - no touch/connect +// 1 = passive vertex - yes touch/connect +// 2 = adjacent vertex - yes but pay attention segmenting a line here +// +export function svgPassiveVertex(node, graph, activeID) { + if (!activeID) return 1; + if (activeID === node.id) return 0; + + var parents = graph.parentWays(node); + + for (var i = 0; i < parents.length; i++) { + var nodes = parents[i].nodes; + var isClosed = parents[i].isClosed(); + for (var j = 0; j < nodes.length; j++) { // find this vertex, look nearby + if (nodes[j] === node.id) { + var ix1 = j - 2; + var ix2 = j - 1; + var ix3 = j + 1; + var ix4 = j + 2; + + if (isClosed) { // wraparound if needed + var max = nodes.length - 1; + if (ix1 < 0) ix1 = max + ix1; + if (ix2 < 0) ix2 = max + ix2; + if (ix3 > max) ix3 = ix3 - max; + if (ix4 > max) ix4 = ix4 - max; + } + + if (nodes[ix1] === activeID) return 0; // no - prevent self intersect + else if (nodes[ix2] === activeID) return 2; // ok - adjacent + else if (nodes[ix3] === activeID) return 2; // ok - adjacent + else if (nodes[ix4] === activeID) return 0; // no - prevent self intersect + else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // no - prevent self intersect + } + } + } + + return 1; // ok +} + + +export function svgOneWaySegments(projection, graph, dt) { + return function(entity) { + var i = 0; + var offset = dt; + var segments = []; + var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; + var coordinates = graph.childNodes(entity).map(function(n) { return n.loc; }); + var a, b; + + if (entity.tags.oneway === '-1') { + coordinates.reverse(); + } + + d3_geoStream({ + type: 'LineString', + coordinates: coordinates + }, projection.stream(clip({ + lineStart: function() {}, + lineEnd: function() { a = null; }, + point: function(x, y) { + b = [x, y]; + + if (a) { + var span = geoVecLength(a, b) - offset; + + if (span >= 0) { + var angle = Math.atan2(b[1] - a[1], b[0] - a[0]); + var dx = dt * Math.cos(angle); + var dy = dt * Math.sin(angle); + var p = [ + a[0] + offset * Math.cos(angle), + a[1] + offset * Math.sin(angle) + ]; + var segment = 'M' + a[0] + ',' + a[1] + 'L' + p[0] + ',' + p[1]; + + for (span -= dt; span >= 0; span -= dt) { + p[0] += dx; + p[1] += dy; + segment += 'L' + p[0] + ',' + p[1]; + } + + segment += 'L' + b[0] + ',' + b[1]; + segments.push({id: entity.id, index: i, d: segment}); + } + + offset = -span; + i++; + } + + a = b; + } + }))); + + return segments; + }; +} + + +export function svgPath(projection, graph, isArea) { + + // Explanation of magic numbers: + // "padding" here allows space for strokes to extend beyond the viewport, + // so that the stroke isn't drawn along the edge of the viewport when + // the shape is clipped. + // + // When drawing lines, pad viewport by 5px. + // When drawing areas, pad viewport by 65px in each direction to allow + // for 60px area fill stroke (see ".fill-partial path.fill" css rule) + + var cache = {}; + var padding = isArea ? 65 : 5; + var viewport = projection.clipExtent(); + var paddedExtent = [ + [viewport[0][0] - padding, viewport[0][1] - padding], + [viewport[1][0] + padding, viewport[1][1] + padding] + ]; + var clip = d3_geoIdentity().clipExtent(paddedExtent).stream; + var project = projection.stream; + var path = d3_geoPath() + .projection({stream: function(output) { return project(clip(output)); }}); + + var svgpath = function(entity) { + if (entity.id in cache) { + return cache[entity.id]; + } else { + return cache[entity.id] = path(entity.asGeoJSON(graph)); + } + }; + + svgpath.geojson = path; + + return svgpath; +} + + +export function svgPointTransform(projection) { + var svgpoint = function(entity) { + // http://jsperf.com/short-array-join + var pt = projection(entity.loc); + return 'translate(' + pt[0] + ',' + pt[1] + ')'; + }; + + svgpoint.geojson = function(d) { + return svgpoint(d.properties.entity); + }; + + return svgpoint; +} + + +export function svgRelationMemberTags(graph) { + return function(entity) { + var tags = entity.tags; + graph.parentRelations(entity).forEach(function(relation) { + var type = relation.tags.type; + if (type === 'multipolygon' || type === 'boundary') { + tags = _extend({}, relation.tags, tags); + } + }); + return tags; + }; +} + + +export function svgSegmentWay(way, graph, activeID) { + var features = { passive: [], active: [] }; + var coordGroups = { passive: [], active: [] }; + var nodeGroups = { passive: [], active: [] }; + var coords = []; + var nodes = []; + var startType = null; // 0 = active, 1 = passive, 2 = adjacent + var currType = null; // 0 = active, 1 = passive, 2 = adjacent + var node; + + for (var i = 0; i < way.nodes.length; i++) { + if (way.nodes[i] === activeID) { // vertex is the activeID + coords = []; // draw no segment here + nodes = []; + startType = null; + continue; + } + + node = graph.entity(way.nodes[i]); + currType = svgPassiveVertex(node, graph, activeID); + + if (startType === null) { + startType = currType; + } + + if (currType !== startType) { // line changes here - try to save a segment + + if (coords.length > 0) { // finish previous segment + coords.push(node.loc); + nodes.push(node.id); + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); + } else { + coordGroups.passive.push(coords); + nodeGroups.passive.push(nodes); + } + } + + coords = []; + nodes = []; + startType = currType; + } + + coords.push(node.loc); + nodes.push(node.id); + } + + // complete whatever segment we ended on + if (coords.length > 1) { + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); + } else { + coordGroups.passive.push(coords); + nodeGroups.passive.push(nodes); + } + } + + if (coordGroups.passive.length) { + features.passive.push({ + type: 'Feature', + id: way.id, + properties: { + target: true, + entity: way, + nodes: nodeGroups.passive + }, + geometry: { + type: 'MultiLineString', + coordinates: coordGroups.passive + } + }); + } + + if (coordGroups.active.length) { + features.active.push({ + type: 'Feature', + id: way.id + '-nope', // break the ids on purpose + properties: { + target: true, + entity: way, + nodes: nodeGroups.active, + nope: true, + originalID: way.id + }, + geometry: { + type: 'MultiLineString', + coordinates: coordGroups.active + } + }); + } + + return features; +} diff --git a/modules/svg/index.js b/modules/svg/index.js index 8f54f5a5e..68e2f2f28 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -9,13 +9,15 @@ export { svgLines } from './lines.js'; export { svgMapillaryImages } from './mapillary_images.js'; export { svgMapillarySigns } from './mapillary_signs.js'; export { svgMidpoints } from './midpoints.js'; -export { svgOneWaySegments } from './one_way_segments.js'; +export { svgOneWaySegments } from './helpers.js'; export { svgOpenstreetcamImages } from './openstreetcam_images.js'; export { svgOsm } from './osm.js'; -export { svgPath } from './path.js'; -export { svgPointTransform } from './point_transform.js'; +export { svgPassiveVertex } from './helpers.js'; +export { svgPath } from './helpers.js'; +export { svgPointTransform } from './helpers.js'; export { svgPoints } from './points.js'; -export { svgRelationMemberTags } from './relation_member_tags.js'; +export { svgRelationMemberTags } from './helpers.js'; +export { svgSegmentWay } from './helpers.js'; export { svgTagClasses } from './tag_classes.js'; export { svgTurns } from './turns.js'; export { svgVertices } from './vertices.js'; diff --git a/modules/svg/labels.js b/modules/svg/labels.js index a42022721..c79448d75 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -10,10 +10,11 @@ import { textDirection } from '../util/locale'; import { geoExtent, - geoEuclideanDistance, - geoInterp, geoPolygonIntersectsPolygon, - geoPathLength + geoPathLength, + geoScaleToZoom, + geoVecInterp, + geoVecLength } from '../geo'; import { osmEntity } from '../osm'; @@ -27,14 +28,15 @@ import { } from '../util'; + export function svgLabels(projection, context) { - var path = d3_geoPath(projection), - detected = utilDetect(), - baselineHack = (detected.ie || detected.browser.toLowerCase() === 'edge'), - rdrawn = rbush(), - rskipped = rbush(), - textWidthCache = {}, - entitybboxes = {}; + var path = d3_geoPath(projection); + var detected = utilDetect(); + var baselineHack = (detected.ie || detected.browser.toLowerCase() === 'edge'); + var _rdrawn = rbush(); + var _rskipped = rbush(); + var _textWidthCache = {}; + var _entitybboxes = {}; // Listed from highest to lowest priority var labelStack = [ @@ -87,8 +89,8 @@ export function svgLabels(projection, context) { function textWidth(text, size, elem) { - var c = textWidthCache[size]; - if (!c) c = textWidthCache[size] = {}; + var c = _textWidthCache[size]; + if (!c) c = _textWidthCache[size] = {}; if (c[text]) { return c[text]; @@ -113,9 +115,11 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit paths.exit() .remove(); + // enter/update paths.enter() .append('path') .style('stroke-width', get(labels, 'font-size')) @@ -131,9 +135,11 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit texts.exit() .remove(); + // enter texts.enter() .append('text') .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; }) @@ -141,9 +147,8 @@ export function svgLabels(projection, context) { .append('textPath') .attr('class', 'textpath'); - texts = selection.selectAll('text.' + classes); - - texts.selectAll('.textpath') + // update + selection.selectAll('text.' + classes).selectAll('.textpath') .filter(filter) .data(entities, osmEntity.key) .attr('startOffset', '50%') @@ -157,17 +162,17 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit texts.exit() .remove(); - texts = texts.enter() + // enter/update + texts.enter() .append('text') .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; }) - .merge(texts); - - texts + .merge(texts) .attr('x', get(labels, 'x')) .attr('y', get(labels, 'y')) .style('text-anchor', get(labels, 'textAnchor')) @@ -194,25 +199,25 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit icons.exit() .remove(); - icons = icons.enter() + // enter/update + icons.enter() .append('use') .attr('class', 'icon ' + classes) .attr('width', '17px') .attr('height', '17px') - .merge(icons); - - icons + .merge(icons) .attr('transform', get(labels, 'transform')) .attr('xlink:href', function(d) { - var preset = context.presets().match(d, context.graph()), - picon = preset && preset.icon; + var preset = context.presets().match(d, context.graph()); + var picon = preset && preset.icon; - if (!picon) + if (!picon) { return ''; - else { + } else { var isMaki = dataFeatureIcons.indexOf(picon) !== -1; return '#' + picon + (isMaki ? '-15' : ''); } @@ -221,23 +226,11 @@ export function svgLabels(projection, context) { function drawCollisionBoxes(selection, rtree, which) { - var showDebug = context.getDebug('collision'), - classes = 'debug ' + which + ' ' + - (which === 'debug-skipped' ? 'orange' : 'yellow'); + var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow'); - var debug = selection.selectAll('.layer-label-debug') - .data(showDebug ? [true] : []); - - debug.exit() - .remove(); - - debug = debug.enter() - .append('g') - .attr('class', 'layer-label-debug') - .merge(debug); - - if (showDebug) { - var gj = rtree.all().map(function(d) { + var gj = []; + if (context.getDebug('collision')) { + gj = rtree.all().map(function(d) { return { type: 'Polygon', coordinates: [[ [d.minX, d.minY], [d.maxX, d.minY], @@ -246,67 +239,102 @@ export function svgLabels(projection, context) { [d.minX, d.minY] ]]}; }); - - var debugboxes = debug.selectAll('.' + which) - .data(gj); - - debugboxes.exit() - .remove(); - - debugboxes = debugboxes.enter() - .append('path') - .attr('class', classes) - .merge(debugboxes); - - debugboxes - .attr('d', d3_geoPath()); } + + var boxes = selection.selectAll('.' + which) + .data(gj); + + // exit + boxes.exit() + .remove(); + + // enter/update + boxes.enter() + .append('path') + .attr('class', classes) + .merge(boxes) + .attr('d', d3_geoPath()); } function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) { - var lowZoom = context.surface().classed('low-zoom'); + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = geoScaleToZoom(projection.scale()); + + var labelable = []; + var renderNodeAs = {}; + var i, j, k, entity, geometry; - var labelable = [], i, j, k, entity, geometry; for (i = 0; i < labelStack.length; i++) { labelable.push([]); } if (fullRedraw) { - rdrawn.clear(); - rskipped.clear(); - entitybboxes = {}; + _rdrawn.clear(); + _rskipped.clear(); + _entitybboxes = {}; + } else { for (i = 0; i < entities.length; i++) { entity = entities[i]; var toRemove = [] - .concat(entitybboxes[entity.id] || []) - .concat(entitybboxes[entity.id + 'I'] || []); + .concat(_entitybboxes[entity.id] || []) + .concat(_entitybboxes[entity.id + 'I'] || []); for (j = 0; j < toRemove.length; j++) { - rdrawn.remove(toRemove[j]); - rskipped.remove(toRemove[j]); + _rdrawn.remove(toRemove[j]); + _rskipped.remove(toRemove[j]); } } } - // Split entities into groups specified by labelStack + // Loop through all the entities to do some preprocessing for (i = 0; i < entities.length; i++) { entity = entities[i]; geometry = entity.geometry(graph); - if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point - var preset = geometry === 'area' && context.presets().match(entity, graph), - icon = preset && !blacklisted(preset) && preset.icon; + // Insert collision boxes around interesting points/vertices + if (geometry === 'point' || (geometry === 'vertex' && isInterestingVertex(entity))) { + var hasDirections = entity.directions(graph, projection).length; + var markerPadding; + + if (!wireframe && geometry === 'point' && !(zoom >= 18 && hasDirections)) { + renderNodeAs[entity.id] = 'point'; + markerPadding = 20; // extra y for marker height + } else { + renderNodeAs[entity.id] = 'vertex'; + markerPadding = 0; + } + + var coord = projection(entity.loc); + var nodePadding = 10; + var bbox = { + minX: coord[0] - nodePadding, + minY: coord[1] - nodePadding - markerPadding, + maxX: coord[0] + nodePadding, + maxY: coord[1] + nodePadding + }; + + doInsert(bbox, entity.id + 'P'); + } + + // From here on, treat vertices like points + if (geometry === 'vertex') { + geometry = 'point'; + } + + // Determine which entities are label-able + var preset = geometry === 'area' && context.presets().match(entity, graph); + var icon = preset && !blacklisted(preset) && preset.icon; if (!icon && !utilDisplayName(entity)) continue; for (k = 0; k < labelStack.length; k++) { - var matchGeom = labelStack[k][0], - matchKey = labelStack[k][1], - matchVal = labelStack[k][2], - hasVal = entity.tags[matchKey]; + var matchGeom = labelStack[k][0]; + var matchKey = labelStack[k][1]; + var matchVal = labelStack[k][2]; + var hasVal = entity.tags[matchKey]; if (geometry === matchGeom && hasVal && (matchVal === '*' || matchVal === hasVal)) { labelable[k].push(entity); @@ -330,22 +358,28 @@ export function svgLabels(projection, context) { // Try and find a valid label for labellable entities for (k = 0; k < labelable.length; k++) { var fontSize = labelStack[k][3]; + for (i = 0; i < labelable[k].length; i++) { entity = labelable[k][i]; geometry = entity.geometry(graph); - var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName, - name = getName(entity), - width = name && textWidth(name, fontSize), - p = null; + var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName; + var name = getName(entity); + var width = name && textWidth(name, fontSize); + var p = null; + + if (geometry === 'point' || geometry === 'vertex') { + // no point or vertex labels in wireframe mode + // no vertex labels at low zooms (vertices have no icons) + if (wireframe) continue; + var renderAs = renderNodeAs[entity.id]; + if (renderAs === 'vertex' && zoom < 17) continue; + + p = getPointLabel(entity, width, fontSize, renderAs); - if (geometry === 'point') { - p = getPointLabel(entity, width, fontSize, geometry); - } else if (geometry === 'vertex' && !lowZoom) { - // don't label vertices at low zoom because they don't have icons - p = getPointLabel(entity, width, fontSize, geometry); } else if (geometry === 'line') { p = getLineLabel(entity, width, fontSize); + } else if (geometry === 'area') { p = getAreaLabel(entity, width, fontSize); } @@ -360,38 +394,52 @@ export function svgLabels(projection, context) { } + function isInterestingVertex(entity) { + var selectedIDs = context.selectedIDs(); + + return entity.hasInterestingTags() || + entity.isEndpoint(graph) || + entity.isConnected(graph) || + selectedIDs.indexOf(entity.id) !== -1 || + _some(graph.parentWays(entity), function(parent) { + return selectedIDs.indexOf(parent.id) !== -1; + }); + } + + function getPointLabel(entity, width, height, geometry) { - var y = (geometry === 'point' ? -12 : 0), - pointOffsets = { - ltr: [15, y, 'start'], - rtl: [-15, y, 'end'] - }; + var y = (geometry === 'point' ? -12 : 0); + var pointOffsets = { + ltr: [15, y, 'start'], + rtl: [-15, y, 'end'] + }; - var coord = projection(entity.loc), - margin = 2, - offset = pointOffsets[textDirection], - p = { - height: height, - width: width, - x: coord[0] + offset[0], - y: coord[1] + offset[1], - textAnchor: offset[2] - }, - bbox; + var coord = projection(entity.loc); + var textPadding = 2; + var offset = pointOffsets[textDirection]; + var p = { + height: height, + width: width, + x: coord[0] + offset[0], + y: coord[1] + offset[1], + textAnchor: offset[2] + }; + // insert a collision box for the text label.. + var bbox; if (textDirection === 'rtl') { bbox = { - minX: p.x - width - margin, - minY: p.y - (height / 2) - margin, - maxX: p.x + margin, - maxY: p.y + (height / 2) + margin + minX: p.x - width - textPadding, + minY: p.y - (height / 2) - textPadding, + maxX: p.x + textPadding, + maxY: p.y + (height / 2) + textPadding }; } else { bbox = { - minX: p.x - margin, - minY: p.y - (height / 2) - margin, - maxX: p.x + width + margin, - maxY: p.y + (height / 2) + margin + minX: p.x - textPadding, + minY: p.y - (height / 2) - textPadding, + maxX: p.x + width + textPadding, + maxY: p.y + (height / 2) + textPadding }; } @@ -402,26 +450,28 @@ export function svgLabels(projection, context) { function getLineLabel(entity, width, height) { - var viewport = geoExtent(context.projection.clipExtent()).polygon(), - nodes = _map(graph.childNodes(entity), 'loc').map(projection), - length = geoPathLength(nodes); + var viewport = geoExtent(context.projection.clipExtent()).polygon(); + var points = _map(graph.childNodes(entity), 'loc').map(projection); + var length = geoPathLength(points); if (length < width + 20) return; + // todo: properly clip points to viewport + // % along the line to attempt to place the label var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70, 25, 75, 20, 80, 15, 95, 10, 90, 5, 95]; - var margin = 3; + var padding = 3; for (var i = 0; i < lineOffsets.length; i++) { - var offset = lineOffsets[i], - middle = offset / 100 * length, - start = middle - width / 2; + var offset = lineOffsets[i]; + var middle = offset / 100 * length; + var start = middle - width / 2; if (start < 0 || start + width > length) continue; // generate subpath and ignore paths that are invalid or don't cross viewport. - var sub = subpath(nodes, start, start + width); + var sub = subpath(points, start, start + width); if (!sub || !geoPolygonIntersectsPolygon(viewport, sub, true)) { continue; } @@ -431,20 +481,22 @@ export function svgLabels(projection, context) { sub = sub.reverse(); } - var bboxes = [], - boxsize = (height + 2) / 2; + var bboxes = []; + var boxsize = (height + 2) / 2; for (var j = 0; j < sub.length - 1; j++) { var a = sub[j]; var b = sub[j + 1]; - var num = Math.max(1, Math.floor(geoEuclideanDistance(a, b) / boxsize / 2)); + + // split up the text into small collision boxes + var num = Math.max(1, Math.floor(geoVecLength(a, b) / boxsize / 2)); for (var box = 0; box < num; box++) { - var p = geoInterp(a, b, box / num); - var x0 = p[0] - boxsize - margin; - var y0 = p[1] - boxsize - margin; - var x1 = p[0] + boxsize + margin; - var y1 = p[1] + boxsize + margin; + var p = geoVecInterp(a, b, box / num); + var x0 = p[0] - boxsize - padding; + var y0 = p[1] - boxsize - padding; + var x1 = p[0] + boxsize + padding; + var y1 = p[1] + boxsize + padding; bboxes.push({ minX: Math.min(x0, x1), @@ -455,7 +507,7 @@ export function svgLabels(projection, context) { } } - if (tryInsert(bboxes, entity.id, false)) { + if (tryInsert(bboxes, entity.id, false)) { // accept this one return { 'font-size': height + 2, lineString: lineString(sub), @@ -469,18 +521,18 @@ export function svgLabels(projection, context) { return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2); } - function lineString(nodes) { - return 'M' + nodes.join('L'); + function lineString(points) { + return 'M' + points.join('L'); } - function subpath(nodes, from, to) { - var sofar = 0, - start, end, i0, i1; + function subpath(points, from, to) { + var sofar = 0; + var start, end, i0, i1; - for (var i = 0; i < nodes.length - 1; i++) { - var a = nodes[i], - b = nodes[i + 1]; - var current = geoEuclideanDistance(a, b); + for (var i = 0; i < points.length - 1; i++) { + var a = points[i]; + var b = points[i + 1]; + var current = geoVecLength(a, b); var portion; if (!start && sofar + current >= from) { portion = (from - sofar) / current; @@ -501,30 +553,30 @@ export function svgLabels(projection, context) { sofar += current; } - var ret = nodes.slice(i0, i1); - ret.unshift(start); - ret.push(end); - return ret; + var result = points.slice(i0, i1); + result.unshift(start); + result.push(end); + return result; } } function getAreaLabel(entity, width, height) { - var centroid = path.centroid(entity.asGeoJSON(graph, true)), - extent = entity.extent(graph), - areaWidth = projection(extent[1])[0] - projection(extent[0])[0]; + var centroid = path.centroid(entity.asGeoJSON(graph, true)); + var extent = entity.extent(graph); + var areaWidth = projection(extent[1])[0] - projection(extent[0])[0]; if (isNaN(centroid[0]) || areaWidth < 20) return; - var preset = context.presets().match(entity, context.graph()), - picon = preset && preset.icon, - iconSize = 17, - margin = 2, - p = {}; + var preset = context.presets().match(entity, context.graph()); + var picon = preset && preset.icon; + var iconSize = 17; + var padding = 2; + var p = {}; if (picon) { // icon and label.. if (addIcon()) { - addLabel(iconSize + margin); + addLabel(iconSize + padding); return p; } } else { // label only.. @@ -556,10 +608,10 @@ export function svgLabels(projection, context) { var labelX = centroid[0]; var labelY = centroid[1] + yOffset; var bbox = { - minX: labelX - (width / 2) - margin, - minY: labelY - (height / 2) - margin, - maxX: labelX + (width / 2) + margin, - maxY: labelY + (height / 2) + margin + minX: labelX - (width / 2) - padding, + minY: labelY - (height / 2) - padding, + maxX: labelX + (width / 2) + padding, + maxY: labelY + (height / 2) + padding }; if (tryInsert([bbox], entity.id, true)) { @@ -575,12 +627,25 @@ export function svgLabels(projection, context) { } + // force insert a singular bounding box + // singular box only, no array, id better be unique + function doInsert(bbox, id) { + bbox.id = id; + + var oldbox = _entitybboxes[id]; + if (oldbox) { + _rdrawn.remove(oldbox); + } + _entitybboxes[id] = bbox; + _rdrawn.insert(bbox); + } + + function tryInsert(bboxes, id, saveSkipped) { - var skipped = false, - bbox; + var skipped = false; for (var i = 0; i < bboxes.length; i++) { - bbox = bboxes[i]; + var bbox = bboxes[i]; bbox.id = id; // Check that label is visible @@ -588,28 +653,30 @@ export function svgLabels(projection, context) { skipped = true; break; } - if (rdrawn.collides(bbox)) { + if (_rdrawn.collides(bbox)) { skipped = true; break; } } - entitybboxes[id] = bboxes; + _entitybboxes[id] = bboxes; if (skipped) { if (saveSkipped) { - rskipped.load(bboxes); + _rskipped.load(bboxes); } } else { - rdrawn.load(bboxes); + _rdrawn.load(bboxes); } return !skipped; } - var label = selection.selectAll('.layer-label'), - halo = selection.selectAll('.layer-halo'); + var layer = selection.selectAll('.layer-labels'); + var halo = layer.selectAll('.layer-labels-halo'); + var label = layer.selectAll('.layer-labels-label'); + var debug = layer.selectAll('.layer-labels-debug'); // points drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point); @@ -627,51 +694,74 @@ export function svgLabels(projection, context) { drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo', positions.area); // debug - drawCollisionBoxes(label, rskipped, 'debug-skipped'); - drawCollisionBoxes(label, rdrawn, 'debug-drawn'); + drawCollisionBoxes(debug, _rskipped, 'debug-skipped'); + drawCollisionBoxes(debug, _rdrawn, 'debug-drawn'); - selection.call(filterLabels); + layer.call(filterLabels); } function filterLabels(selection) { var layers = selection - .selectAll('.layer-label, .layer-halo'); + .selectAll('.layer-labels-label, .layer-labels-halo'); - layers.selectAll('.proximate') - .classed('proximate', false); + layers.selectAll('.nolabel') + .classed('nolabel', false); - var mouse = context.mouse(), - graph = context.graph(), - selectedIDs = context.selectedIDs(), - ids = [], - pad, bbox; + var mouse = context.mouse(); + var graph = context.graph(); + var selectedIDs = context.selectedIDs(); + var ids = []; + var pad, bbox; // hide labels near the mouse if (mouse) { pad = 20; bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad }; - ids.push.apply(ids, _map(rdrawn.search(bbox), 'id')); + ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); } - // hide labels along selected ways, or near selected vertices + // hide labels on selected nodes (they look weird when dragging / haloed) for (var i = 0; i < selectedIDs.length; i++) { var entity = graph.hasEntity(selectedIDs[i]); - if (!entity) continue; - var geometry = entity.geometry(graph); - - if (geometry === 'line') { + if (entity && entity.type === 'node') { ids.push(selectedIDs[i]); - } else if (geometry === 'vertex') { - var point = context.projection(entity.loc); - pad = 10; - bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad }; - ids.push.apply(ids, _map(rdrawn.search(bbox), 'id')); } } layers.selectAll(utilEntitySelector(ids)) - .classed('proximate', true); + .classed('nolabel', true); + + + // draw the mouse bbox if debugging is on.. + var debug = selection.selectAll('.layer-labels-debug'); + var gj = []; + if (context.getDebug('collision')) { + gj = bbox ? [{ + type: 'Polygon', + coordinates: [[ + [bbox.minX, bbox.minY], + [bbox.maxX, bbox.minY], + [bbox.maxX, bbox.maxY], + [bbox.minX, bbox.maxY], + [bbox.minX, bbox.minY] + ]] + }] : []; + } + + var box = debug.selectAll('.debug-mouse') + .data(gj); + + // exit + box.exit() + .remove(); + + // enter/update + box.enter() + .append('path') + .attr('class', 'debug debug-mouse yellow') + .merge(box) + .attr('d', d3_geoPath()); } diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 1646f083b..727d0178b 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -10,6 +10,7 @@ import { svgOneWaySegments, svgPath, svgRelationMemberTags, + svgSegmentWay, svgTagClasses } from './index'; @@ -36,13 +37,63 @@ export function svgLines(projection, context) { }; + function drawTargets(selection, graph, entities, filter) { + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var getPath = svgPath(projection).geojson; + var activeID = context.activeID(); + + // The targets and nopes will be MultiLineString sub-segments of the ways + var data = { targets: [], nopes: [] }; + + entities.forEach(function(way) { + var features = svgSegmentWay(way, graph, activeID); + data.targets.push.apply(data.targets, features.passive); + data.nopes.push.apply(data.nopes, features.active); + }); + + + // Targets allow hover and vertex snapping + var targets = selection.selectAll('.line.target-allowed') + .filter(function(d) { return filter(d.properties.entity); }) + .data(data.targets, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('path') + .merge(targets) + .attr('d', getPath) + .attr('class', function(d) { return 'way line target target-allowed ' + targetClass + d.id; }); + + + // NOPE + var nopes = selection.selectAll('.line.target-nope') + .filter(function(d) { return filter(d.properties.entity); }) + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('path') + .merge(nopes) + .attr('d', getPath) + .attr('class', function(d) { return 'way line target target-nope ' + nopeClass + d.id; }); + } + + function drawLines(selection, graph, entities, filter) { - function waystack(a, b) { - var selected = context.selectedIDs(), - scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0, - scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0; + var selected = context.selectedIDs(); + var scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0; + var scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0; if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; } if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; } @@ -51,6 +102,11 @@ export function svgLines(projection, context) { function drawLineGroup(selection, klass, isSelected) { + // Note: Don't add `.selected` class in draw modes + var mode = context.mode(); + var isDrawing = mode && /^draw/.test(mode.id); + var selectedClass = (!isDrawing && isSelected) ? 'selected ' : ''; + var lines = selection .selectAll('path') .filter(filter) @@ -59,13 +115,13 @@ export function svgLines(projection, context) { lines.exit() .remove(); - // Optimization: call simple TagClasses only on enter selection. This + // Optimization: Call expensive TagClasses only on enter selection. This // works because osmEntity.key is defined to include the entity v attribute. lines.enter() .append('path') .attr('class', function(d) { - return 'way line ' + klass + ' ' + d.id + (isSelected ? ' selected' : '') + - (oldMultiPolygonOuters[d.id] ? ' old-multipolygon' : ''); + var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : ''; + return 'way line ' + klass + ' ' + selectedClass + oldMPClass + d.id; }) .call(svgTagClasses()) .merge(lines) @@ -91,15 +147,15 @@ export function svgLines(projection, context) { } - var getPath = svgPath(projection, graph), - ways = [], - pathdata = {}, - onewaydata = {}, - oldMultiPolygonOuters = {}; + var getPath = svgPath(projection, graph); + var ways = []; + var pathdata = {}; + var onewaydata = {}; + var oldMultiPolygonOuters = {}; for (var i = 0; i < entities.length; i++) { - var entity = entities[i], - outer = osmSimpleMultipolygonOuterMember(entity, graph); + var entity = entities[i]; + var outer = osmSimpleMultipolygonOuterMember(entity, graph); if (outer) { ways.push(entity.mergeTags(outer.tags)); oldMultiPolygonOuters[outer.id] = true; @@ -117,7 +173,7 @@ export function svgLines(projection, context) { }); - var layer = selection.selectAll('.layer-lines'); + var layer = selection.selectAll('.layer-lines .layer-lines-lines'); var layergroup = layer .selectAll('g.layergroup') @@ -164,8 +220,8 @@ export function svgLines(projection, context) { .selectAll('path') .filter(filter) .data( - function() { return onewaydata[this.parentNode.__data__] || []; }, - function(d) { return [d.id, d.index]; } + function data() { return onewaydata[this.parentNode.__data__] || []; }, + function key(d) { return [d.id, d.index]; } ); oneways.exit() @@ -181,6 +237,11 @@ export function svgLines(projection, context) { if (detected.ie) { oneways.each(function() { this.parentNode.insertBefore(this, this); }); } + + + // touch targets + selection.selectAll('.layer-lines .layer-lines-targets') + .call(drawTargets, graph, ways, filter); } diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 1bae3436c..fb8a4b4b8 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -1,23 +1,16 @@ import _throttle from 'lodash-es/throttle'; - -import { - geoIdentity as d3_geoIdentity, - geoPath as d3_geoPath -} from 'd3-geo'; - import { select as d3_select } from 'd3-selection'; - -import { svgPointTransform } from './point_transform'; +import { svgPath, svgPointTransform } from './index'; import { services } from '../services'; export function svgMapillaryImages(projection, context, dispatch) { - var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), - minZoom = 12, - minMarkerZoom = 16, - minViewfieldZoom = 18, - layer = d3_select(null), - _mapillary; + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var minMarkerZoom = 16; + var minViewfieldZoom = 18; + var layer = d3_select(null); + var _mapillary; function init() { @@ -128,25 +121,19 @@ export function svgMapillaryImages(projection, context, dispatch) { var sequences = (service ? service.sequences(projection) : []); var images = (service && showMarkers ? service.images(projection) : []); - var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; - var project = projection.stream; - var makePath = d3_geoPath().projection({ stream: function(output) { - return project(clip(output)); - }}); - var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); + // exit traces.exit() .remove(); + // enter/update traces = traces.enter() .append('path') .attr('class', 'sequence') - .merge(traces); - - traces - .attr('d', makePath); + .merge(traces) + .attr('d', svgPath(projection).geojson); var groups = layer.selectAll('.markers').selectAll('.viewfield-group') diff --git a/modules/svg/mapillary_signs.js b/modules/svg/mapillary_signs.js index a327c108d..4b6095cd2 100644 --- a/modules/svg/mapillary_signs.js +++ b/modules/svg/mapillary_signs.js @@ -5,10 +5,10 @@ import { services } from '../services'; export function svgMapillarySigns(projection, context, dispatch) { - var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), - minZoom = 12, - layer = d3_select(null), - _mapillary; + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var layer = d3_select(null); + var _mapillary; function init() { diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index 101625fee..0f68d949c 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -7,25 +7,68 @@ import { import { geoAngle, - geoEuclideanDistance, - geoInterp, - geoLineIntersection + geoLineIntersection, + geoVecInterp, + geoVecLength } from '../geo'; export function svgMidpoints(projection, context) { + var targetRadius = 8; - return function drawMidpoints(selection, graph, entities, filter, extent) { - var layer = selection.selectAll('.layer-hit'); + function drawTargets(selection, graph, entities, filter) { + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var getTransform = svgPointTransform(projection).geojson; + + var data = entities.map(function(midpoint) { + return { + type: 'Feature', + id: midpoint.id, + properties: { + target: true, + entity: midpoint + }, + geometry: { + type: 'Point', + coordinates: midpoint.loc + } + }; + }); + + var targets = selection.selectAll('.midpoint.target') + .filter(function(d) { return filter(d.properties.entity); }) + .data(data, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('circle') + .attr('r', targetRadius) + .merge(targets) + .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; }) + .attr('transform', getTransform); + } + + + function drawMidpoints(selection, graph, entities, filter, extent) { + var layer = selection.selectAll('.layer-points .layer-points-midpoints'); var mode = context.mode(); if (mode && mode.id !== 'select') { - layer.selectAll('g.midpoint').remove(); + layer.selectAll('g.midpoint') + .remove(); + + selection.selectAll('.layer-points .layer-points-targets .midpoint.target') + .remove(); + return; } - var poly = extent.polygon(), - midpoints = {}; + var poly = extent.polygon(); + var midpoints = {}; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -40,16 +83,16 @@ export function svgMidpoints(projection, context) { var nodes = graph.childNodes(entity); for (var j = 0; j < nodes.length - 1; j++) { - var a = nodes[j], - b = nodes[j + 1], - id = [a.id, b.id].sort().join('-'); + var a = nodes[j]; + var b = nodes[j + 1]; + var id = [a.id, b.id].sort().join('-'); if (midpoints[id]) { midpoints[id].parents.push(entity); } else { - if (geoEuclideanDistance(projection(a.loc), projection(b.loc)) > 40) { - var point = geoInterp(a.loc, b.loc, 0.5), - loc = null; + if (geoVecLength(projection(a.loc), projection(b.loc)) > 40) { + var point = geoVecInterp(a.loc, b.loc, 0.5); + var loc = null; if (extent.intersects(point)) { loc = point; @@ -57,8 +100,8 @@ export function svgMidpoints(projection, context) { for (var k = 0; k < 4; k++) { point = geoLineIntersection([a.loc, b.loc], [poly[k], poly[k + 1]]); if (point && - geoEuclideanDistance(projection(a.loc), projection(point)) > 20 && - geoEuclideanDistance(projection(b.loc), projection(point)) > 20) + geoVecLength(projection(a.loc), projection(point)) > 20 && + geoVecLength(projection(b.loc), projection(point)) > 20) { loc = point; break; @@ -107,22 +150,24 @@ export function svgMidpoints(projection, context) { .insert('g', ':first-child') .attr('class', 'midpoint'); - enter.append('polygon') + enter + .append('polygon') .attr('points', '-6,8 10,0 -6,-8') .attr('class', 'shadow'); - enter.append('polygon') + enter + .append('polygon') .attr('points', '-3,4 5,0 -3,-4') .attr('class', 'fill'); groups = groups .merge(enter) .attr('transform', function(d) { - var translate = svgPointTransform(projection), - a = graph.entity(d.edge[0]), - b = graph.entity(d.edge[1]), - angleVal = Math.round(geoAngle(a, b, projection) * (180 / Math.PI)); - return translate(d) + ' rotate(' + angleVal + ')'; + var translate = svgPointTransform(projection); + var a = graph.entity(d.edge[0]); + var b = graph.entity(d.edge[1]); + var angle = geoAngle(a, b, projection) * (180 / Math.PI); + return translate(d) + ' rotate(' + angle + ')'; }) .call(svgTagClasses().tags( function(d) { return d.parents[0].tags; } @@ -132,5 +177,11 @@ export function svgMidpoints(projection, context) { groups.select('polygon.shadow'); groups.select('polygon.fill'); - }; + + // Draw touch targets.. + selection.selectAll('.layer-points .layer-points-targets') + .call(drawTargets, graph, _values(midpoints), midpointFilter); + } + + return drawMidpoints; } diff --git a/modules/svg/one_way_segments.js b/modules/svg/one_way_segments.js deleted file mode 100644 index 66759d66d..000000000 --- a/modules/svg/one_way_segments.js +++ /dev/null @@ -1,67 +0,0 @@ -import { - geoIdentity as d3_geoIdentity, - geoStream as d3_geoStream -} from 'd3-geo'; - -import { geoEuclideanDistance } from '../geo'; - - -export function svgOneWaySegments(projection, graph, dt) { - return function(entity) { - var a, - b, - i = 0, - offset = dt, - segments = [], - clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream, - coordinates = graph.childNodes(entity).map(function(n) { - return n.loc; - }); - - if (entity.tags.oneway === '-1') coordinates.reverse(); - - d3_geoStream({ - type: 'LineString', - coordinates: coordinates - }, projection.stream(clip({ - lineStart: function() {}, - lineEnd: function() { - a = null; - }, - point: function(x, y) { - b = [x, y]; - - if (a) { - var span = geoEuclideanDistance(a, b) - offset; - - if (span >= 0) { - var angle = Math.atan2(b[1] - a[1], b[0] - a[0]), - dx = dt * Math.cos(angle), - dy = dt * Math.sin(angle), - p = [a[0] + offset * Math.cos(angle), - a[1] + offset * Math.sin(angle)]; - - var segment = 'M' + a[0] + ',' + a[1] + - 'L' + p[0] + ',' + p[1]; - - for (span -= dt; span >= 0; span -= dt) { - p[0] += dx; - p[1] += dy; - segment += 'L' + p[0] + ',' + p[1]; - } - - segment += 'L' + b[0] + ',' + b[1]; - segments.push({id: entity.id, index: i, d: segment}); - } - - offset = -span; - i++; - } - - a = b; - } - }))); - - return segments; - }; -} diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index 3184a7bc2..35fc76cc0 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -1,23 +1,16 @@ import _throttle from 'lodash-es/throttle'; - -import { - geoIdentity as d3_geoIdentity, - geoPath as d3_geoPath -} from 'd3-geo'; - import { select as d3_select } from 'd3-selection'; - -import { svgPointTransform } from './point_transform'; +import { svgPath, svgPointTransform } from './index'; import { services } from '../services'; export function svgOpenstreetcamImages(projection, context, dispatch) { - var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), - minZoom = 12, - minMarkerZoom = 16, - minViewfieldZoom = 18, - layer = d3_select(null), - _openstreetcam; + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var minMarkerZoom = 16; + var minViewfieldZoom = 18; + var layer = d3_select(null); + var _openstreetcam; function init() { @@ -128,25 +121,19 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { var sequences = (service ? service.sequences(projection) : []); var images = (service && showMarkers ? service.images(projection) : []); - var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; - var project = projection.stream; - var makePath = d3_geoPath().projection({ stream: function(output) { - return project(clip(output)); - }}); - var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); + // exit traces.exit() .remove(); + // enter/update traces = traces.enter() .append('path') .attr('class', 'sequence') - .merge(traces); - - traces - .attr('d', makePath); + .merge(traces) + .attr('d', svgPath(projection).geojson); var groups = layer.selectAll('.markers').selectAll('.viewfield-group') diff --git a/modules/svg/osm.js b/modules/svg/osm.js index 2a3b6ff72..db9a26a12 100644 --- a/modules/svg/osm.js +++ b/modules/svg/osm.js @@ -4,10 +4,34 @@ export function svgOsm(projection, context, dispatch) { function drawOsm(selection) { selection.selectAll('.layer-osm') - .data(['areas', 'lines', 'hit', 'halo', 'label']) + .data(['areas', 'lines', 'points', 'labels']) .enter() .append('g') .attr('class', function(d) { return 'layer-osm layer-' + d; }); + + selection.selectAll('.layer-areas').selectAll('.layer-areas-group') + .data(['areas', 'targets']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-areas-group layer-areas-' + d; }); + + selection.selectAll('.layer-lines').selectAll('.layer-lines-group') + .data(['lines', 'targets']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-lines-group layer-lines-' + d; }); + + selection.selectAll('.layer-points').selectAll('.layer-points-group') + .data(['points', 'midpoints', 'vertices', 'turns', 'targets']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-points-group layer-points-' + d; }); + + selection.selectAll('.layer-labels').selectAll('.layer-labels-group') + .data(['halo', 'label', 'debug']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-labels-group layer-labels-' + d; }); } diff --git a/modules/svg/path.js b/modules/svg/path.js deleted file mode 100644 index d2e522995..000000000 --- a/modules/svg/path.js +++ /dev/null @@ -1,37 +0,0 @@ -import { - geoIdentity as d3_geoIdentity, - geoPath as d3_geoPath -} from 'd3-geo'; - - -export function svgPath(projection, graph, isArea) { - - // Explanation of magic numbers: - // "padding" here allows space for strokes to extend beyond the viewport, - // so that the stroke isn't drawn along the edge of the viewport when - // the shape is clipped. - // - // When drawing lines, pad viewport by 5px. - // When drawing areas, pad viewport by 65px in each direction to allow - // for 60px area fill stroke (see ".fill-partial path.fill" css rule) - - var cache = {}, - padding = isArea ? 65 : 5, - viewport = projection.clipExtent(), - paddedExtent = [ - [viewport[0][0] - padding, viewport[0][1] - padding], - [viewport[1][0] + padding, viewport[1][1] + padding] - ], - clip = d3_geoIdentity().clipExtent(paddedExtent).stream, - project = projection.stream, - path = d3_geoPath() - .projection({stream: function(output) { return project(clip(output)); }}); - - return function(entity) { - if (entity.id in cache) { - return cache[entity.id]; - } else { - return cache[entity.id] = path(entity.asGeoJSON(graph)); - } - }; -} diff --git a/modules/svg/point_transform.js b/modules/svg/point_transform.js deleted file mode 100644 index cf80f845d..000000000 --- a/modules/svg/point_transform.js +++ /dev/null @@ -1,7 +0,0 @@ -export function svgPointTransform(projection) { - return function(entity) { - // http://jsperf.com/short-array-join - var pt = projection(entity.loc); - return 'translate(' + pt[0] + ',' + pt[1] + ')'; - }; -} diff --git a/modules/svg/points.js b/modules/svg/points.js index e48f31163..c2f1fc0eb 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -1,6 +1,5 @@ -import _filter from 'lodash-es/filter'; - import { dataFeatureIcons } from '../../data'; +import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform, svgTagClasses } from './index'; @@ -19,19 +18,77 @@ export function svgPoints(projection, context) { } - return function drawPoints(selection, graph, entities, filter) { - var wireframe = context.surface().classed('fill-wireframe'), - points = wireframe ? [] : _filter(entities, function(e) { - return e.geometry(graph) === 'point'; + // Avoid exit/enter if we're just moving stuff around. + // The node will get a new version but we only need to run the update selection. + function fastEntityKey(d) { + var mode = context.mode(); + var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); + return isMoving ? d.id : osmEntity.key(d); + } + + + function drawTargets(selection, graph, entities, filter) { + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var getTransform = svgPointTransform(projection).geojson; + var activeID = context.activeID(); + var data = []; + + entities.forEach(function(node) { + if (activeID === node.id) return; // draw no target on the activeID + + data.push({ + type: 'Feature', + id: node.id, + properties: { + target: true, + entity: node + }, + geometry: node.asGeoJSON() }); + }); + + var targets = selection.selectAll('.point.target') + .filter(function(d) { return filter(d.properties.entity); }) + .data(data, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('rect') + .attr('x', -10) + .attr('y', -26) + .attr('width', 20) + .attr('height', 30) + .merge(targets) + .attr('class', function(d) { return 'node point target ' + fillClass + d.id; }) + .attr('transform', getTransform); + } + + + function drawPoints(selection, graph, entities, filter) { + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = geoScaleToZoom(projection.scale()); + + // points with a direction will render as vertices at higher zooms + function renderAsPoint(entity) { + return entity.geometry(graph) === 'point' && + !(zoom >= 18 && entity.directions(graph, projection).length); + } + + // all points will render as vertices in wireframe mode too + var points = wireframe ? [] : entities.filter(renderAsPoint); points.sort(sortY); - var layer = selection.selectAll('.layer-hit'); + + var layer = selection.selectAll('.layer-points .layer-points-points'); var groups = layer.selectAll('g.point') .filter(filter) - .data(points, osmEntity.key); + .data(points, fastEntityKey); groups.exit() .remove(); @@ -41,20 +98,24 @@ export function svgPoints(projection, context) { .attr('class', function(d) { return 'node point ' + d.id; }) .order(); - enter.append('path') + enter + .append('path') .call(markerPath, 'shadow'); - enter.append('ellipse') + enter + .append('ellipse') .attr('cx', 0.5) .attr('cy', 1) .attr('rx', 6.5) .attr('ry', 3) .attr('class', 'stroke'); - enter.append('path') + enter + .append('path') .call(markerPath, 'stroke'); - enter.append('use') + enter + .append('use') .attr('transform', 'translate(-5, -19)') .attr('class', 'icon') .attr('width', '11px') @@ -71,8 +132,8 @@ export function svgPoints(projection, context) { groups.select('.stroke'); groups.select('.icon') .attr('xlink:href', function(entity) { - var preset = context.presets().match(entity, graph), - picon = preset && preset.icon; + var preset = context.presets().match(entity, graph); + var picon = preset && preset.icon; if (!picon) return ''; @@ -81,5 +142,13 @@ export function svgPoints(projection, context) { return '#' + picon + (isMaki ? '-11' : ''); } }); - }; + + + // touch targets + selection.selectAll('.layer-points .layer-points-targets') + .call(drawTargets, graph, points, filter); + } + + + return drawPoints; } diff --git a/modules/svg/relation_member_tags.js b/modules/svg/relation_member_tags.js deleted file mode 100644 index 824d56fe0..000000000 --- a/modules/svg/relation_member_tags.js +++ /dev/null @@ -1,15 +0,0 @@ -import _extend from 'lodash-es/extend'; - - -export function svgRelationMemberTags(graph) { - return function(entity) { - var tags = entity.tags; - graph.parentRelations(entity).forEach(function(relation) { - var type = relation.tags.type; - if (type === 'multipolygon' || type === 'boundary') { - tags = _extend({}, relation.tags, tags); - } - }); - return tags; - }; -} diff --git a/modules/svg/turns.js b/modules/svg/turns.js index 537ccf5ff..cd16bd0dd 100644 --- a/modules/svg/turns.js +++ b/modules/svg/turns.js @@ -18,7 +18,8 @@ export function svgTurns(projection) { (!turn.indirect_restriction && /^only_/.test(restriction) ? 'only' : 'no') + u; } - var groups = selection.selectAll('.layer-hit').selectAll('g.turn') + var layer = selection.selectAll('.layer-points .layer-points-turns'); + var groups = layer.selectAll('g.turn') .data(turns, key); groups.exit() diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 053fc5302..c6843a3af 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,208 +1,422 @@ +import _assign from 'lodash-es/assign'; import _values from 'lodash-es/values'; +import { select as d3_select } from 'd3-selection'; + import { dataFeatureIcons } from '../../data'; +import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; -import { svgPointTransform } from './index'; +import { svgPassiveVertex, svgPointTransform } from './index'; export function svgVertices(projection, context) { var radiuses = { - // z16-, z17, z18+, tagged - shadow: [6, 7.5, 7.5, 11.5], - stroke: [2.5, 3.5, 3.5, 7], - fill: [1, 1.5, 1.5, 1.5] + // z16-, z17, z18+, w/icon + shadow: [6, 7.5, 7.5, 12], + stroke: [2.5, 3.5, 3.5, 8], + fill: [1, 1.5, 1.5, 1.5] }; - var hover; + var _currHoverTarget; + var _currPersistent = {}; + var _currHover = {}; + var _prevHover = {}; + var _currSelected = {}; + var _prevSelected = {}; + var _radii = {}; - function siblingAndChildVertices(ids, graph, extent) { - var vertices = {}; + function sortY(a, b) { + return b.loc[1] - a.loc[1]; + } - function addChildVertices(entity) { - if (!context.features().isHiddenFeature(entity, graph, entity.geometry(graph))) { - var i; - if (entity.type === 'way') { - for (i = 0; i < entity.nodes.length; i++) { - addChildVertices(graph.entity(entity.nodes[i])); - } - } else if (entity.type === 'relation') { - for (i = 0; i < entity.members.length; i++) { - var member = context.hasEntity(entity.members[i].id); - if (member) { - addChildVertices(member); - } - } - } else if (entity.intersects(extent, graph)) { - vertices[entity.id] = entity; - } - } - } - - ids.forEach(function(id) { - var entity = context.hasEntity(id); - if (entity && entity.type === 'node') { - vertices[entity.id] = entity; - context.graph().parentWays(entity).forEach(function(entity) { - addChildVertices(entity); - }); - } else if (entity) { - addChildVertices(entity); - } - }); - - return vertices; + // Avoid exit/enter if we're just moving stuff around. + // The node will get a new version but we only need to run the update selection. + function fastEntityKey(d) { + var mode = context.mode(); + var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); + return isMoving ? d.id : osmEntity.key(d); } - function draw(selection, vertices, klass, graph, zoom, siblings) { + function draw(selection, graph, vertices, sets, filter) { + sets = sets || { selected: {}, important: {}, hovered: {} }; - function icon(entity) { + var icons = {}; + var directions = {}; + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = geoScaleToZoom(projection.scale()); + var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); + + + function getIcon(entity) { if (entity.id in icons) return icons[entity.id]; + icons[entity.id] = entity.hasInterestingTags() && context.presets().match(entity, graph).icon; return icons[entity.id]; } - function setClass(klass) { - return function(entity) { - this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id); - }; + + // memoize directions results, return false for empty arrays (for use in filter) + function getDirections(entity) { + if (entity.id in directions) return directions[entity.id]; + + var angles = entity.directions(graph, projection); + directions[entity.id] = angles.length ? angles : false; + return angles; } - function setAttributes(selection) { - ['shadow','stroke','fill'].forEach(function(klass) { + + function updateAttributes(selection) { + ['shadow', 'stroke', 'fill'].forEach(function(klass) { var rads = radiuses[klass]; selection.selectAll('.' + klass) .each(function(entity) { - var i = z && icon(entity), - c = i ? 0.5 : 0, - r = rads[i ? 3 : z]; + var i = z && getIcon(entity); + var r = rads[i ? 3 : z]; // slightly increase the size of unconnected endpoints #3775 if (entity.isEndpoint(graph) && !entity.isConnected(graph)) { r += 1.5; } - this.setAttribute('cx', c); - this.setAttribute('cy', -c); - this.setAttribute('r', r); - if (i && klass === 'fill') { - this.setAttribute('visibility', 'hidden'); - } else { - this.removeAttribute('visibility'); + if (klass === 'shadow') { // remember this value, so we don't need to + _radii[entity.id] = r; // recompute it when we draw the touch targets } + + d3_select(this) + .attr('r', r) + .attr('visibility', (i && klass === 'fill') ? 'hidden' : null); }); }); selection.selectAll('use') - .each(function() { - if (z) { - this.removeAttribute('visibility'); - } else { - this.setAttribute('visibility', 'hidden'); - } - }); + .attr('visibility', (z === 0 ? 'hidden' : null)); } + vertices.sort(sortY); - siblings = siblings || {}; - - var icons = {}, - z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); - - var groups = selection - .data(vertices, osmEntity.key); + var groups = selection.selectAll('g.vertex') + .filter(filter) + .data(vertices, fastEntityKey); + // exit groups.exit() .remove(); + // enter var enter = groups.enter() .append('g') - .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); + .attr('class', function(d) { return 'node vertex ' + d.id; }) + .order(); - enter.append('circle') - .each(setClass('shadow')); + enter + .append('circle') + .attr('class', 'shadow'); - enter.append('circle') - .each(setClass('stroke')); + enter + .append('circle') + .attr('class', 'stroke'); // Vertices with icons get a `use`. - enter.filter(function(d) { return icon(d); }) + enter.filter(function(d) { return getIcon(d); }) .append('use') - .attr('transform', 'translate(-5, -6)') - .attr('xlink:href', function(d) { - var picon = icon(d), - isMaki = dataFeatureIcons.indexOf(picon) !== -1; - return '#' + picon + (isMaki ? '-11' : ''); - }) + .attr('class', 'icon') .attr('width', '11px') .attr('height', '11px') - .each(setClass('icon')); + .attr('transform', 'translate(-5.5, -5.5)') + .attr('xlink:href', function(d) { + var picon = getIcon(d); + var isMaki = dataFeatureIcons.indexOf(picon) !== -1; + return '#' + picon + (isMaki ? '-11' : ''); + }); // Vertices with tags get a fill. enter.filter(function(d) { return d.hasInterestingTags(); }) .append('circle') - .each(setClass('fill')); + .attr('class', 'fill'); - groups + // update + groups = groups .merge(enter) .attr('transform', svgPointTransform(projection)) - .classed('sibling', function(entity) { return entity.id in siblings; }) - .classed('shared', function(entity) { return graph.isShared(entity); }) - .classed('endpoint', function(entity) { return entity.isEndpoint(graph); }) - .call(setAttributes); + .classed('sibling', function(d) { return d.id in sets.selected; }) + .classed('shared', function(d) { return graph.isShared(d); }) + .classed('endpoint', function(d) { return d.isEndpoint(graph); }) + .call(updateAttributes); + + + // Directional vertices get viewfields + var dgroups = groups.filter(function(d) { return getDirections(d); }) + .selectAll('.viewfieldgroup') + .data(function data(d) { return zoom < 18 ? [] : [d]; }, osmEntity.key); + + // exit + dgroups.exit() + .remove(); + + // enter/update + dgroups = dgroups.enter() + .insert('g', '.shadow') + .attr('class', 'viewfieldgroup') + .merge(dgroups); + + var viewfields = dgroups.selectAll('.viewfield') + .data(getDirections, function key(d) { return d; }); + + // exit + viewfields.exit() + .remove(); + + // enter/update + viewfields.enter() + .append('path') + .attr('class', 'viewfield') + .attr('d', 'M0,0H0') + .merge(viewfields) + .attr('marker-start', 'url(#viewfield-marker' + (wireframe ? '-wireframe' : '') + ')') + .attr('transform', function(d) { return 'rotate(' + d + ')'; }); } - function drawVertices(selection, graph, entities, filter, extent, zoom) { - var siblings = siblingAndChildVertices(context.selectedIDs(), graph, extent), - wireframe = context.surface().classed('fill-wireframe'), - vertices = []; + function drawTargets(selection, graph, entities, filter) { + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var getTransform = svgPointTransform(projection).geojson; + var activeID = context.activeID(); + var data = { targets: [], nopes: [] }; - for (var i = 0; i < entities.length; i++) { - var entity = entities[i], - geometry = entity.geometry(graph); + entities.forEach(function(node) { + if (activeID === node.id) return; // draw no target on the activeID - if (wireframe && geometry === 'point') { - vertices.push(entity); - continue; + var vertexType = svgPassiveVertex(node, graph, activeID); + if (vertexType !== 0) { // passive or adjacent - allow to connect + data.targets.push({ + type: 'Feature', + id: node.id, + properties: { + target: true, + entity: node + }, + geometry: node.asGeoJSON() + }); + } else { + data.nopes.push({ + type: 'Feature', + id: node.id + '-nope', // break the ids on purpose + properties: { + target: true, + entity: node, + nope: true, + originalID: node.id + }, + geometry: node.asGeoJSON() + }); } + }); - if (geometry !== 'vertex') - continue; - if (entity.id in siblings || - entity.hasInterestingTags() || - entity.isEndpoint(graph) || - entity.isConnected(graph)) { - vertices.push(entity); + // Targets allow hover and vertex snapping + var targets = selection.selectAll('.vertex.target-allowed') + .filter(function(d) { return filter(d.properties.entity); }) + .data(data.targets, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('circle') + .attr('r', function(d) { return (_radii[d.id] || radiuses.shadow[3]); }) + .merge(targets) + .attr('class', function(d) { return 'node vertex target target-allowed ' + targetClass + d.id; }) + .attr('transform', getTransform); + + + // NOPE + var nopes = selection.selectAll('.vertex.target-nope') + .filter(function(d) { return filter(d.properties.entity); }) + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('circle') + .attr('r', function(d) { return (_radii[d.properties.originalID] || radiuses.shadow[3]); }) + .merge(nopes) + .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; }) + .attr('transform', getTransform); + } + + + // Points can also render as vertices: + // 1. in wireframe mode or + // 2. at higher zooms if they have a direction + function renderAsVertex(entity, graph, wireframe, zoom) { + var geometry = entity.geometry(graph); + return geometry === 'vertex' || (geometry === 'point' && ( + wireframe || (zoom > 18 && entity.directions(graph, projection).length) + )); + } + + + function getSiblingAndChildVertices(ids, graph, wireframe, zoom) { + var results = {}; + + function addChildVertices(entity) { + var geometry = entity.geometry(graph); + if (!context.features().isHiddenFeature(entity, graph, geometry)) { + var i; + if (entity.type === 'way') { + for (i = 0; i < entity.nodes.length; i++) { + var child = graph.hasEntity(entity.nodes[i]); + if (child) { + addChildVertices(child); + } + } + } else if (entity.type === 'relation') { + for (i = 0; i < entity.members.length; i++) { + var member = graph.hasEntity(entity.members[i].id); + if (member) { + addChildVertices(member); + } + } + } else if (renderAsVertex(entity, graph, wireframe, zoom)) { + results[entity.id] = entity; + } } } - var layer = selection.selectAll('.layer-hit'); - layer.selectAll('g.vertex.vertex-persistent') - .filter(filter) - .call(draw, vertices, 'vertex-persistent', graph, zoom, siblings); + ids.forEach(function(id) { + var entity = graph.hasEntity(id); + if (!entity) return; - drawHover(selection, graph, extent, zoom); + if (entity.type === 'node') { + if (renderAsVertex(entity, graph, wireframe, zoom)) { + results[entity.id] = entity; + graph.parentWays(entity).forEach(function(entity) { + addChildVertices(entity); + }); + } + } else { // way, relation + addChildVertices(entity); + } + }); + + return results; } - function drawHover(selection, graph, extent, zoom) { - var hovered = hover ? siblingAndChildVertices([hover.id], graph, extent) : {}; - var layer = selection.selectAll('.layer-hit'); + function drawVertices(selection, graph, entities, filter, extent, fullRedraw) { + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = geoScaleToZoom(projection.scale()); + var mode = context.mode(); + var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); - layer.selectAll('g.vertex.vertex-hover') - .call(draw, _values(hovered), 'vertex-hover', graph, zoom); + if (fullRedraw) { + _currPersistent = {}; + _radii = {}; + } + + // Collect important vertices from the `entities` list.. + // (during a paritial redraw, it will not contain everything) + for (var i = 0; i < entities.length; i++) { + var entity = entities[i]; + var geometry = entity.geometry(graph); + var keep = false; + + // a point that looks like a vertex.. + if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { + _currPersistent[entity.id] = entity; + keep = true; + + // a vertex of some importance.. + } else if (geometry === 'vertex' && + (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph))) { + _currPersistent[entity.id] = entity; + keep = true; + } + + // whatever this is, it's not a persistent vertex.. + if (!keep && !fullRedraw) { + delete _currPersistent[entity.id]; + } + } + + // 3 sets of vertices to consider: + var sets = { + persistent: _currPersistent, // persistent = important vertices (render always) + selected: _currSelected, // selected + siblings of selected (render always) + hovered: _currHover // hovered + siblings of hovered (render only in draw modes) + }; + + var all = _assign({}, (isMoving ? _currHover : {}), _currSelected, _currPersistent); + + // Draw the vertices.. + // The filter function controls the scope of what objects d3 will touch (exit/enter/update) + // Adjust the filter function to expand the scope beyond whatever entities were passed in. + var filterRendered = function(d) { + return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d); + }; + selection.selectAll('.layer-points .layer-points-vertices') + .call(draw, graph, currentVisible(all), sets, filterRendered); + + // Draw touch targets.. + // When drawing, render all targets (not just those affected by a partial redraw) + var filterTouch = function(d) { + return isMoving ? true : filterRendered(d); + }; + selection.selectAll('.layer-points .layer-points-targets') + .call(drawTargets, graph, currentVisible(all), filterTouch); + + + function currentVisible(which) { + return Object.keys(which) + .map(graph.hasEntity, graph) // the current version of this entity + .filter(function (entity) { return entity && entity.intersects(extent, graph); }); + } } - drawVertices.drawHover = function(selection, graph, target, extent, zoom) { - if (target === hover) return; - hover = target; - drawHover(selection, graph, extent, zoom); + // partial redraw - only update the selected items.. + drawVertices.drawSelected = function(selection, graph, extent) { + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = geoScaleToZoom(projection.scale()); + + _prevSelected = _currSelected || {}; + _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom); + + // note that drawVertices will add `_currSelected` automatically if needed.. + var filter = function(d) { return d.id in _prevSelected; }; + drawVertices(selection, graph, _values(_prevSelected), filter, extent, false); + }; + + + // partial redraw - only update the hovered items.. + drawVertices.drawHover = function(selection, graph, target, extent) { + if (target === _currHoverTarget) return; // continue only if something changed + + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = geoScaleToZoom(projection.scale()); + + _prevHover = _currHover || {}; + _currHoverTarget = target; + + if (_currHoverTarget) { + _currHover = getSiblingAndChildVertices([_currHoverTarget.id], graph, wireframe, zoom); + } else { + _currHover = {}; + } + + // note that drawVertices will add `_currHover` automatically if needed.. + var filter = function(d) { return d.id in _prevHover; }; + drawVertices(selection, graph, _values(_prevHover), filter, extent, false); }; return drawVertices; diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index b0856c56f..f29503bec 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoRoundCoords } from '../geo'; +import { geoVecFloor } from '../geo'; import { textDirection } from '../util/locale'; import { uiTooltipHtml } from './tooltipHtml'; @@ -81,7 +81,7 @@ export function uiEditMenu(context, operations) { .attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; }) .classed('disabled', function (d) { return d.disabled(); }) .attr('transform', function (d, i) { - return 'translate(' + geoRoundCoords([ + return 'translate(' + geoVecFloor([ 0, m + i * buttonHeight ]).join(',') + ')'; diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index f70b47f48..dde654d45 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -26,7 +26,8 @@ import { import { geoExtent, - geoRawMercator + geoRawMercator, + geoZoomToScale } from '../../geo'; import { @@ -46,12 +47,12 @@ import { export function uiFieldRestrictions(field, context) { - var dispatch = d3_dispatch('change'), - breathe = behaviorBreathe(context), - hover = behaviorHover(context), - initialized = false, - vertexID, - fromNodeID; + var dispatch = d3_dispatch('change'); + var breathe = behaviorBreathe(context); + var hover = behaviorHover(context); + var initialized = false; + var vertexID; + var fromNodeID; function restrictions(selection) { @@ -73,19 +74,18 @@ export function uiFieldRestrictions(field, context) { .attr('class', 'restriction-help'); - var intersection = osmIntersection(context.graph(), vertexID), - graph = intersection.graph, - vertex = graph.entity(vertexID), - filter = utilFunctor(true), - extent = geoExtent(), - projection = geoRawMercator(); + var intersection = osmIntersection(context.graph(), vertexID); + var graph = intersection.graph; + var vertex = graph.entity(vertexID); + var filter = utilFunctor(true); + var projection = geoRawMercator(); - var d = utilGetDimensions(wrap.merge(enter)), - c = [d[0] / 2, d[1] / 2], - z = 24; + var d = utilGetDimensions(wrap.merge(enter)); + var c = [d[0] / 2, d[1] / 2]; + var z = 24; projection - .scale(256 * Math.pow(2, z) / (2 * Math.PI)); + .scale(geoZoomToScale(z)); var s = projection(vertex.loc); @@ -93,10 +93,12 @@ export function uiFieldRestrictions(field, context) { .translate([c[0] - s[0], c[1] - s[1]]) .clipExtent([[0, 0], d]); - var drawLayers = svgLayers(projection, context).only('osm').dimensions(d), - drawVertices = svgVertices(projection, context), - drawLines = svgLines(projection, context), - drawTurns = svgTurns(projection, context); + var extent = geoExtent(projection.invert([0, d[1]]), projection.invert([d[0], 0])); + + var drawLayers = svgLayers(projection, context).only('osm').dimensions(d); + var drawVertices = svgVertices(projection, context); + var drawLines = svgLines(projection, context); + var drawTurns = svgTurns(projection, context); enter .call(drawLayers); @@ -115,7 +117,7 @@ export function uiFieldRestrictions(field, context) { surface .call(utilSetDimensions, d) - .call(drawVertices, graph, [vertex], filter, extent, z) + .call(drawVertices, graph, [vertex], filter, extent, true) .call(drawLines, graph, intersection.ways, filter) .call(drawTurns, graph, intersection.turns(fromNodeID)); @@ -152,9 +154,13 @@ export function uiFieldRestrictions(field, context) { .call(breathe); var datum = d3_event.target.__data__; + var entity = datum && datum.properties && datum.properties.entity; + if (entity) datum = entity; + if (datum instanceof osmEntity) { fromNodeID = intersection.adjacentNodeId(datum.id); render(); + } else if (datum instanceof osmTurn) { if (datum.restriction) { context.perform( @@ -174,9 +180,9 @@ export function uiFieldRestrictions(field, context) { function mouseover() { var datum = d3_event.target.__data__; if (datum instanceof osmTurn) { - var graph = context.graph(), - presets = context.presets(), - preset; + var graph = context.graph(); + var presets = context.presets(); + var preset; if (datum.restriction) { preset = presets.match(graph.entity(datum.restriction), graph); diff --git a/modules/ui/map_in_map.js b/modules/ui/map_in_map.js index 4a6609c98..08ca8b44d 100644 --- a/modules/ui/map_in_map.js +++ b/modules/ui/map_in_map.js @@ -13,45 +13,44 @@ import { import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; -import { svgDebug, svgGpx } from '../svg'; -import { geoRawMercator } from '../geo'; +import { + geoRawMercator, + geoScaleToZoom, + geoVecSubtract, + geoVecScale, + geoZoomToScale, +} from '../geo'; + import { rendererTileLayer } from '../renderer'; +import { svgDebug, svgGpx } from '../svg'; import { utilSetTransform } from '../util'; import { utilGetDimensions } from '../util/dimensions'; -var TAU = 2 * Math.PI; -function ztok(z) { return 256 * Math.pow(2, z) / TAU; } -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } -function vecSub(a, b) { return [ a[0] - b[0], a[1] - b[1] ]; } -function vecScale(a, b) { return [ a[0] * b, a[1] * b ]; } - - export function uiMapInMap(context) { - function map_in_map(selection) { - var backgroundLayer = rendererTileLayer(context), - overlayLayers = {}, - projection = geoRawMercator(), - gpxLayer = svgGpx(projection, context).showLabels(false), - debugLayer = svgDebug(projection, context), - zoom = d3_zoom() - .scaleExtent([ztok(0.5), ztok(24)]) - .on('start', zoomStarted) - .on('zoom', zoomed) - .on('end', zoomEnded), - isTransformed = false, - isHidden = true, - skipEvents = false, - gesture = null, - zDiff = 6, // by default, minimap renders at (main zoom - 6) - wrap = d3_select(null), - tiles = d3_select(null), - viewport = d3_select(null), - tStart, // transform at start of gesture - tCurr, // transform at most recent event - timeoutId; + var backgroundLayer = rendererTileLayer(context); + var overlayLayers = {}; + var projection = geoRawMercator(); + var gpxLayer = svgGpx(projection, context).showLabels(false); + var debugLayer = svgDebug(projection, context); + var zoom = d3_zoom() + .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)]) + .on('start', zoomStarted) + .on('zoom', zoomed) + .on('end', zoomEnded); + var isTransformed = false; + var isHidden = true; + var skipEvents = false; + var gesture = null; + var zDiff = 6; // by default, minimap renders at (main zoom - 6) + var wrap = d3_select(null); + var tiles = d3_select(null); + var viewport = d3_select(null); + var tStart; // transform at start of gesture + var tCurr; // transform at most recent event + var timeoutId; function zoomStarted() { @@ -64,11 +63,11 @@ export function uiMapInMap(context) { function zoomed() { if (skipEvents) return; - var x = d3_event.transform.x, - y = d3_event.transform.y, - k = d3_event.transform.k, - isZooming = (k !== tStart.k), - isPanning = (x !== tStart.x || y !== tStart.y); + var x = d3_event.transform.x; + var y = d3_event.transform.y; + var k = d3_event.transform.k; + var isZooming = (k !== tStart.k); + var isPanning = (x !== tStart.x || y !== tStart.y); if (!isZooming && !isPanning) { return; // no change @@ -79,12 +78,12 @@ export function uiMapInMap(context) { gesture = isZooming ? 'zoom' : 'pan'; } - var tMini = projection.transform(), - tX, tY, scale; + var tMini = projection.transform(); + var tX, tY, scale; if (gesture === 'zoom') { - var dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); scale = k / tMini.k; tX = (cMini[0] / scale - cMini[0]) * scale; tY = (cMini[1] / scale - cMini[1]) * scale; @@ -100,8 +99,8 @@ export function uiMapInMap(context) { isTransformed = true; tCurr = d3_zoomIdentity.translate(x, y).scale(k); - var zMain = ktoz(context.projection.scale()), - zMini = ktoz(k); + var zMain = geoScaleToZoom(context.projection.scale()); + var zMini = geoScaleToZoom(k); zDiff = zMain - zMini; @@ -115,29 +114,29 @@ export function uiMapInMap(context) { updateProjection(); gesture = null; - var dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); context.map().center(projection.invert(cMini)); // recenter main map.. } function updateProjection() { - var loc = context.map().center(), - dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5), - tMain = context.projection.transform(), - zMain = ktoz(tMain.k), - zMini = Math.max(zMain - zDiff, 0.5), - kMini = ztok(zMini); + var loc = context.map().center(); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); + var tMain = context.projection.transform(); + var zMain = geoScaleToZoom(tMain.k); + var zMini = Math.max(zMain - zDiff, 0.5); + var kMini = geoZoomToScale(zMini); projection .translate([tMain.x, tMain.y]) .scale(kMini); - var point = projection(loc), - mouse = (gesture === 'pan') ? vecSub([tCurr.x, tCurr.y], [tStart.x, tStart.y]) : [0, 0], - xMini = cMini[0] - point[0] + tMain.x + mouse[0], - yMini = cMini[1] - point[1] + tMain.y + mouse[1]; + var point = projection(loc); + var mouse = (gesture === 'pan') ? geoVecSubtract([tCurr.x, tCurr.y], [tStart.x, tStart.y]) : [0, 0]; + var xMini = cMini[0] - point[0] + tMain.x + mouse[0]; + var yMini = cMini[1] - point[1] + tMain.y + mouse[1]; projection .translate([xMini, yMini]) @@ -152,7 +151,7 @@ export function uiMapInMap(context) { } zoom - .scaleExtent([ztok(0.5), ztok(zMain - 3)]); + .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(zMain - 3)]); skipEvents = true; wrap.call(zoom.transform, tCurr); @@ -166,8 +165,8 @@ export function uiMapInMap(context) { updateProjection(); - var dMini = utilGetDimensions(wrap), - zMini = ktoz(projection.scale()); + var dMini = utilGetDimensions(wrap); + var zMini = geoScaleToZoom(projection.scale()); // setup tile container tiles = wrap @@ -249,8 +248,8 @@ export function uiMapInMap(context) { // redraw viewport bounding box if (gesture !== 'pan') { - var getPath = d3_geoPath(projection), - bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] }; + var getPath = d3_geoPath(projection); + var bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] }; viewport = wrap.selectAll('.map-in-map-viewport') .data([0]); diff --git a/modules/ui/radial_menu.js b/modules/ui/radial_menu.js index 01b6d2458..3afd4f738 100644 --- a/modules/ui/radial_menu.js +++ b/modules/ui/radial_menu.js @@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoRoundCoords } from '../geo'; +import { geoVecFloor } from '../geo'; import { uiTooltipHtml } from './tooltipHtml'; @@ -58,7 +58,7 @@ export function uiRadialMenu(context, operations) { .attr('class', function(d) { return 'radial-menu-item radial-menu-item-' + d.id; }) .classed('disabled', function(d) { return d.disabled(); }) .attr('transform', function(d, i) { - return 'translate(' + geoRoundCoords([ + return 'translate(' + geoVecFloor([ r * Math.sin(a0 + i * a), r * Math.cos(a0 + i * a)]).join(',') + ')'; }); diff --git a/test/index.html b/test/index.html index 18092851b..2357d5ba1 100644 --- a/test/index.html +++ b/test/index.html @@ -74,6 +74,8 @@ + + diff --git a/test/spec/actions/circularize.js b/test/spec/actions/circularize.js index fb54bbd05..5d7e4c73c 100644 --- a/test/spec/actions/circularize.js +++ b/test/spec/actions/circularize.js @@ -5,7 +5,7 @@ describe('iD.actionCircularize', function () { var points = graph.childNodes(graph.entity(id)) .map(function (n) { return projection(n.loc); }), centroid = d3.polygonCentroid(points), - radius = iD.geoEuclideanDistance(centroid, points[0]), + radius = iD.geoVecLength(centroid, points[0]), estArea = Math.PI * radius * radius, trueArea = Math.abs(d3.polygonArea(points)), pctDiff = (estArea - trueArea) / estArea; @@ -31,10 +31,10 @@ describe('iD.actionCircularize', function () { vector2 = [point2[0] - center[0], point2[1] - center[1]], distance; - distance = iD.geoEuclideanDistance(vector1, [0, 0]); + distance = iD.geoVecLength(vector1, [0, 0]); vector1 = [vector1[0] / distance, vector1[1] / distance]; - distance = iD.geoEuclideanDistance(vector2, [0, 0]); + distance = iD.geoVecLength(vector2, [0, 0]); vector2 = [vector2[0] / distance, vector2[1] / distance]; return 180 / Math.PI * Math.acos(vector1[0] * vector2[0] + vector1[1] * vector2[1]); @@ -106,7 +106,7 @@ describe('iD.actionCircularize', function () { graph = iD.actionCircularize('-', projection)(graph); expect(isCircular('-', graph)).to.be.ok; - expect(iD.geoEuclideanDistance(graph.entity('d').loc, [2, -2])).to.be.lt(0.5); + expect(iD.geoVecLength(graph.entity('d').loc, [2, -2])).to.be.lt(0.5); }); it('creates circle respecting min-angle limit', function() { diff --git a/test/spec/actions/move_node.js b/test/spec/actions/move_node.js index bc28f748d..771f66b4a 100644 --- a/test/spec/actions/move_node.js +++ b/test/spec/actions/move_node.js @@ -1,8 +1,46 @@ describe('iD.actionMoveNode', function () { it('changes a node\'s location', function () { - var node = iD.Node(), - loc = [2, 3], - graph = iD.actionMoveNode(node.id, loc)(iD.Graph([node])); - expect(graph.entity(node.id).loc).to.eql(loc); + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph); + expect(graph.entity('a').loc).to.eql(toLoc); + }); + + describe('transitions', function () { + it('is transitionable', function() { + expect(iD.actionMoveNode().transitionable).to.be.true; + }); + + it('move node at t = 0', function() { + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph, 0); + expect(graph.entity('a').loc[0]).to.be.closeTo(0, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(0, 1e-6); + }); + + it('move node at t = 0.5', function() { + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph, 0.5); + expect(graph.entity('a').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(1.5, 1e-6); + }); + + it('move node at t = 1', function() { + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph, 1); + expect(graph.entity('a').loc[0]).to.be.closeTo(2, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(3, 1e-6); + }); }); }); diff --git a/test/spec/behavior/hover.js b/test/spec/behavior/hover.js index 8ad0cf3d8..4dbc5ac9a 100644 --- a/test/spec/behavior/hover.js +++ b/test/spec/behavior/hover.js @@ -1,108 +1,117 @@ describe('iD.behaviorHover', function() { - var container, context; + var _container; + var _context; + var _graph; beforeEach(function() { - container = d3.select('body').append('div'); - context = { + _container = d3.select('body').append('div'); + _context = { hover: function() {}, - mode: function() { return { id: 'browse' }; } + mode: function() { return { id: 'browse' }; }, + hasEntity: function(d) { return _graph && _graph.hasEntity(d); } }; }); afterEach(function() { - container.remove(); + _container.remove(); + _graph = null; }); describe('#off', function () { it('removes the .hover class from all elements', function () { - container.append('span').attr('class', 'hover'); - container.call(iD.behaviorHover(context).off); - expect(container.select('span').classed('hover')).to.be.false; + _container.append('span').attr('class', 'hover'); + _container.call(iD.behaviorHover(_context).off); + expect(_container.select('span').classed('hover')).to.be.false; }); it('removes the .hover-disabled class from the surface element', function () { - container.attr('class', 'hover-disabled'); - container.call(iD.behaviorHover(context).off); - expect(container.classed('hover-disabled')).to.be.false; + _container.attr('class', 'hover-disabled'); + _container.call(iD.behaviorHover(_context).off); + expect(_container.classed('hover-disabled')).to.be.false; }); }); describe('mouseover', function () { it('adds the .hover class to all elements to which the same datum is bound', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}); + var a = iD.osmNode({id: 'a'}); + var b = iD.osmNode({id: 'b'}); + _graph = iD.coreGraph([a, b]); - container.selectAll('span') + _container.selectAll('span') .data([a, b, a, b]) .enter().append('span').attr('class', function(d) { return d.id; }); - container.call(iD.behaviorHover(context)); - iD.utilTriggerEvent(container.selectAll('.a'), 'mouseover'); + _container.call(iD.behaviorHover(_context)); + iD.utilTriggerEvent(_container.selectAll('.a'), 'mouseover'); - expect(container.selectAll('.a.hover').nodes()).to.have.length(2); - expect(container.selectAll('.b.hover').nodes()).to.have.length(0); + expect(_container.selectAll('.a.hover').nodes()).to.have.length(2); + expect(_container.selectAll('.b.hover').nodes()).to.have.length(0); }); it('adds the .hover class to all members of a relation', function() { - container.selectAll('span') - .data([iD.Relation({id: 'a', members: [{id: 'b'}]}), iD.Node({id: 'b'})]) + var a = iD.osmRelation({id: 'a', members: [{id: 'b'}]}); + var b = iD.osmNode({id: 'b'}); + _graph = iD.coreGraph([a, b]); + + _container.selectAll('span') + .data([a, b]) .enter().append('span').attr('class', function(d) { return d.id; }); - container.call(iD.behaviorHover(context)); - iD.utilTriggerEvent(container.selectAll('.a'), 'mouseover'); + _container.call(iD.behaviorHover(_context)); + iD.utilTriggerEvent(_container.selectAll('.a'), 'mouseover'); - expect(container.selectAll('.a.hover').nodes()).to.have.length(1); - expect(container.selectAll('.b.hover').nodes()).to.have.length(1); + expect(_container.selectAll('.a.hover').nodes()).to.have.length(1); + expect(_container.selectAll('.b.hover').nodes()).to.have.length(1); }); }); describe('mouseout', function () { it('removes the .hover class from all elements', function () { - container.append('span').attr('class', 'hover'); + _container.append('span').attr('class', 'hover'); - container.call(iD.behaviorHover(context)); - iD.utilTriggerEvent(container.selectAll('.hover'), 'mouseout'); + _container.call(iD.behaviorHover(_context)); + iD.utilTriggerEvent(_container.selectAll('.hover'), 'mouseout'); - expect(container.selectAll('.hover').nodes()).to.have.length(0); + expect(_container.selectAll('.hover').nodes()).to.have.length(0); }); }); describe('alt keydown', function () { it('replaces the .hover class with .hover-suppressed', function () { - container.append('span').attr('class', 'hover'); - container.call(iD.behaviorHover(context).altDisables(true)); + _container.append('span').attr('class', 'hover'); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - expect(container.selectAll('.hover').nodes()).to.have.length(0); - expect(container.selectAll('.hover-suppressed').nodes()).to.have.length(1); - happen.keyup(window, {keyCode: 18}); + happen.keydown(window, { keyCode: 18 }); + expect(_container.selectAll('.hover').nodes()).to.have.length(0); + expect(_container.selectAll('.hover-suppressed').nodes()).to.have.length(1); + happen.keyup(window, { keyCode: 18 }); }); it('adds the .hover-disabled class to the surface', function () { - container.call(iD.behaviorHover(context).altDisables(true)); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - expect(container.classed('hover-disabled')).to.be.true; - happen.keyup(window, {keyCode: 18}); + happen.keydown(window, { keyCode: 18 }); + expect(_container.classed('hover-disabled')).to.be.true; + happen.keyup(window, { keyCode: 18 }); }); }); describe('alt keyup', function () { it('replaces the .hover-suppressed class with .hover', function () { - container.append('span').attr('class', 'hover-suppressed'); - container.call(iD.behaviorHover(context).altDisables(true)); + _container.append('span').attr('class', 'hover-suppressed'); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - happen.keyup(window, {keyCode: 18}); - expect(container.selectAll('.hover').nodes()).to.have.length(1); - expect(container.selectAll('.hover-suppressed').nodes()).to.have.length(0); + happen.keydown(window, { keyCode: 18 }); + happen.keyup(window, { keyCode: 18 }); + expect(_container.selectAll('.hover').nodes()).to.have.length(1); + expect(_container.selectAll('.hover-suppressed').nodes()).to.have.length(0); }); it('removes the .hover-disabled class from the surface', function () { - container.call(iD.behaviorHover(context).altDisables(true)); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - happen.keyup(window, {keyCode: 18}); - expect(container.classed('hover-disabled')).to.be.false; + happen.keydown(window, { keyCode: 18 }); + happen.keyup(window, { keyCode: 18 }); + expect(_container.classed('hover-disabled')).to.be.false; }); }); }); diff --git a/test/spec/core/context.js b/test/spec/core/context.js index 04f665400..df3fbfdbf 100644 --- a/test/spec/core/context.js +++ b/test/spec/core/context.js @@ -59,7 +59,8 @@ describe('iD.Context', function() { collision: false, imagery: false, imperial: false, - driveLeft: false + driveLeft: false, + target: false }; expect(context.debugFlags()).to.eql(flags); diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 6b2e0cb85..76c77ca09 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,73 +1,14 @@ -describe('iD.geo', function() { - describe('geoRoundCoords', function() { - it('rounds coordinates', function() { - expect(iD.geoRoundCoords([0.1, 1])).to.eql([0, 1]); - expect(iD.geoRoundCoords([0, 1])).to.eql([0, 1]); - expect(iD.geoRoundCoords([0, 1.1])).to.eql([0, 1]); - }); - }); - - describe('geoInterp', function() { - it('interpolates halfway', function() { - var a = [0, 0], - b = [10, 10]; - expect(iD.geoInterp(a, b, 0.5)).to.eql([5, 5]); - }); - it('interpolates to one side', function() { - var a = [0, 0], - b = [10, 10]; - expect(iD.geoInterp(a, b, 0)).to.eql([0, 0]); - }); - }); - - describe('geoCross', function() { - it('cross product of right hand turn is positive', function() { - var o = [0, 0], - a = [2, 0], - b = [0, 2]; - expect(iD.geoCross(o, a, b)).to.eql(4); - }); - it('cross product of left hand turn is negative', function() { - var o = [0, 0], - a = [2, 0], - b = [0, -2]; - expect(iD.geoCross(o, a, b)).to.eql(-4); - }); - it('cross product of colinear points is zero', function() { - var o = [0, 0], - a = [-2, 0], - b = [2, 0]; - expect(iD.geoCross(o, a, b)).to.equal(0); - }); - }); - - describe('geoEuclideanDistance', function() { - it('distance between two same points is zero', function() { - var a = [0, 0], - b = [0, 0]; - expect(iD.geoEuclideanDistance(a, b)).to.eql(0); - }); - it('a straight 10 unit line is 10', function() { - var a = [0, 0], - b = [10, 0]; - expect(iD.geoEuclideanDistance(a, b)).to.eql(10); - }); - it('a pythagorean triangle is right', function() { - var a = [0, 0], - b = [4, 3]; - expect(iD.geoEuclideanDistance(a, b)).to.eql(5); - }); - }); +describe('iD.geo - geography', function() { describe('geoLatToMeters', function() { it('0 degrees latitude is 0 meters', function() { expect(iD.geoLatToMeters(0)).to.eql(0); }); it('1 degree latitude is approx 111 km', function() { - expect(iD.geoLatToMeters(1)).to.be.within(110E3, 112E3); + expect(iD.geoLatToMeters(1)).to.be.closeTo(111319, 10); }); it('-1 degree latitude is approx -111 km', function() { - expect(iD.geoLatToMeters(-1)).to.be.within(-112E3, -110E3); + expect(iD.geoLatToMeters(-1)).to.be.closeTo(-111319, 10); }); }); @@ -76,21 +17,21 @@ describe('iD.geo', function() { expect(iD.geoLonToMeters(0, 0)).to.eql(0); }); it('distance of 1 degree longitude varies with latitude', function() { - expect(iD.geoLonToMeters(1, 0)).to.be.within(110E3, 112E3); - expect(iD.geoLonToMeters(1, 15)).to.be.within(107E3, 108E3); - expect(iD.geoLonToMeters(1, 30)).to.be.within(96E3, 97E3); - expect(iD.geoLonToMeters(1, 45)).to.be.within(78E3, 79E3); - expect(iD.geoLonToMeters(1, 60)).to.be.within(55E3, 56E3); - expect(iD.geoLonToMeters(1, 75)).to.be.within(28E3, 29E3); + expect(iD.geoLonToMeters(1, 0)).to.be.closeTo(110946, 10); + expect(iD.geoLonToMeters(1, 15)).to.be.closeTo(107165, 10); + expect(iD.geoLonToMeters(1, 30)).to.be.closeTo(96082, 10); + expect(iD.geoLonToMeters(1, 45)).to.be.closeTo(78450, 10); + expect(iD.geoLonToMeters(1, 60)).to.be.closeTo(55473, 10); + expect(iD.geoLonToMeters(1, 75)).to.be.closeTo(28715, 10); expect(iD.geoLonToMeters(1, 90)).to.eql(0); }); it('distance of -1 degree longitude varies with latitude', function() { - expect(iD.geoLonToMeters(-1, 0)).to.be.within(-112E3, -110E3); - expect(iD.geoLonToMeters(-1, -15)).to.be.within(-108E3, -107E3); - expect(iD.geoLonToMeters(-1, -30)).to.be.within(-97E3, -96E3); - expect(iD.geoLonToMeters(-1, -45)).to.be.within(-79E3, -78E3); - expect(iD.geoLonToMeters(-1, -60)).to.be.within(-56E3, -55E3); - expect(iD.geoLonToMeters(-1, -75)).to.be.within(-29E3, -28E3); + expect(iD.geoLonToMeters(-1, -0)).to.be.closeTo(-110946, 10); + expect(iD.geoLonToMeters(-1, -15)).to.be.closeTo(-107165, 10); + expect(iD.geoLonToMeters(-1, -30)).to.be.closeTo(-96082, 10); + expect(iD.geoLonToMeters(-1, -45)).to.be.closeTo(-78450, 10); + expect(iD.geoLonToMeters(-1, -60)).to.be.closeTo(-55473, 10); + expect(iD.geoLonToMeters(-1, -75)).to.be.closeTo(-28715, 10); expect(iD.geoLonToMeters(-1, -90)).to.eql(0); }); }); @@ -100,10 +41,10 @@ describe('iD.geo', function() { expect(iD.geoMetersToLat(0)).to.eql(0); }); it('111 km is approx 1 degree latitude', function() { - expect(iD.geoMetersToLat(111E3)).to.be.within(0.995, 1.005); + expect(iD.geoMetersToLat(111319)).to.be.closeTo(1, 0.0001); }); it('-111 km is approx -1 degree latitude', function() { - expect(iD.geoMetersToLat(-111E3)).to.be.within(-1.005, -0.995); + expect(iD.geoMetersToLat(-111319)).to.be.closeTo(-1, 0.0001); }); }); @@ -112,22 +53,22 @@ describe('iD.geo', function() { expect(iD.geoMetersToLon(0, 0)).to.eql(0); }); it('distance of 1 degree longitude varies with latitude', function() { - expect(iD.geoMetersToLon(111320, 0)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(107551, 15)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(96486, 30)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(78847, 45)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(55800, 60)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(28902, 75)).to.be.within(0.995, 1.005); + expect(iD.geoMetersToLon(110946, 0)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(107165, 15)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(96082, 30)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(78450, 45)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(55473, 60)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(28715, 75)).to.be.closeTo(1, 1e-4); expect(iD.geoMetersToLon(1, 90)).to.eql(0); }); it('distance of -1 degree longitude varies with latitude', function() { - expect(iD.geoMetersToLon(-111320, 0)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-107551, 15)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-96486, 30)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-78847, 45)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-55800, 60)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-28902, 75)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-1, 90)).to.eql(0); + expect(iD.geoMetersToLon(-110946, -0)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-107165, -15)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-96082, -30)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-78450, -45)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-55473, -60)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-28715, -75)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-1, -90)).to.eql(0); }); }); @@ -159,256 +100,48 @@ describe('iD.geo', function() { describe('geoSphericalDistance', function() { it('distance between two same points is zero', function() { - var a = [0, 0], - b = [0, 0]; + var a = [0, 0]; + var b = [0, 0]; expect(iD.geoSphericalDistance(a, b)).to.eql(0); }); it('a straight 1 degree line at the equator is aproximately 111 km', function() { - var a = [0, 0], - b = [1, 0]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(110E3, 112E3); + var a = [0, 0]; + var b = [1, 0]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(110946, 10); }); it('a pythagorean triangle is (nearly) right', function() { - var a = [0, 0], - b = [4, 3]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(555E3, 556E3); + var a = [0, 0]; + var b = [4, 3]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(555282, 10); }); it('east-west distances at high latitude are shorter', function() { - var a = [0, 60], - b = [1, 60]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(55E3, 56E3); + var a = [0, 60]; + var b = [1, 60]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(55473, 10); }); it('north-south distances at high latitude are not shorter', function() { - var a = [0, 60], - b = [0, 61]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(110E3, 112E3); + var a = [0, 60]; + var b = [0, 61]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(111319, 10); }); }); - describe('geoEdgeEqual', function() { - it('returns false for inequal edges', function() { - expect(iD.geoEdgeEqual(['a','b'], ['a','c'])).to.be.false; + describe('geoZoomToScale', function() { + it('converts from zoom to projection scale (tileSize = 256)', function() { + expect(iD.geoZoomToScale(17)).to.be.closeTo(5340353.715440872, 1e-6); }); - - it('returns true for equal edges along same direction', function() { - expect(iD.geoEdgeEqual(['a','b'], ['a','b'])).to.be.true; - }); - - it('returns true for equal edges along opposite direction', function() { - expect(iD.geoEdgeEqual(['a','b'], ['b','a'])).to.be.true; + it('converts from zoom to projection scale (tileSize = 512)', function() { + expect(iD.geoZoomToScale(17, 512)).to.be.closeTo(10680707.430881744, 1e-6); }); }); - describe('geoAngle', function() { - it('returns angle between a and b', function() { - var projection = function (_) { return _; }; - expect(iD.geoAngle({loc:[0, 0]}, {loc:[1, 0]}, projection)).to.be.closeTo(0, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, 1]}, projection)).to.be.closeTo(Math.PI / 2, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[-1, 0]}, projection)).to.be.closeTo(Math.PI, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, -1]}, projection)).to.be.closeTo(-Math.PI / 2, 1e-6); + describe('geoScaleToZoom', function() { + it('converts from projection scale to zoom (tileSize = 256)', function() { + expect(iD.geoScaleToZoom(5340353.715440872)).to.be.closeTo(17, 1e-6); + }); + it('converts from projection scale to zoom (tileSize = 512)', function() { + expect(iD.geoScaleToZoom(10680707.430881744, 512)).to.be.closeTo(17, 1e-6); }); }); - describe('geoRotate', function() { - it('rotates points around [0, 0]', function() { - var points = [[5, 0], [5, 1]], - angle = Math.PI, - around = [0, 0], - result = iD.geoRotate(points, angle, around); - expect(result[0][0]).to.be.closeTo(-5, 1e-6); - expect(result[0][1]).to.be.closeTo(0, 1e-6); - expect(result[1][0]).to.be.closeTo(-5, 1e-6); - expect(result[1][1]).to.be.closeTo(-1, 1e-6); - }); - - it('rotates points around [3, 0]', function() { - var points = [[5, 0], [5, 1]], - angle = Math.PI, - around = [3, 0], - result = iD.geoRotate(points, angle, around); - expect(result[0][0]).to.be.closeTo(1, 1e-6); - expect(result[0][1]).to.be.closeTo(0, 1e-6); - expect(result[1][0]).to.be.closeTo(1, 1e-6); - expect(result[1][1]).to.be.closeTo(-1, 1e-6); - }); - }); - - describe('geoChooseEdge', function() { - var projection = function (l) { return l; }; - projection.invert = projection; - - it('returns undefined properties for a degenerate way (no nodes)', function() { - expect(iD.geoChooseEdge([], [0, 0], projection)).to.eql({ - index: undefined, - distance: Infinity, - loc: undefined - }); - }); - - it('returns undefined properties for a degenerate way (single node)', function() { - expect(iD.geoChooseEdge([iD.Node({loc: [0, 0]})], [0, 0], projection)).to.eql({ - index: undefined, - distance: Infinity, - loc: undefined - }); - }); - - it('calculates the orthogonal projection of a point onto a segment', function() { - // a --*--- b - // | - // c - // - // * = [2, 0] - var a = [0, 0], - b = [5, 0], - c = [2, 1], - nodes = [ - iD.Node({loc: a}), - iD.Node({loc: b}) - ]; - - var choice = iD.geoChooseEdge(nodes, c, projection); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(1); - expect(choice.loc).to.eql([2, 0]); - }); - - it('returns the starting vertex when the orthogonal projection is < 0', function() { - var a = [0, 0], - b = [5, 0], - c = [-3, 4], - nodes = [ - iD.Node({loc: a}), - iD.Node({loc: b}) - ]; - - var choice = iD.geoChooseEdge(nodes, c, projection); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(5); - expect(choice.loc).to.eql([0, 0]); - }); - - it('returns the ending vertex when the orthogonal projection is > 1', function() { - var a = [0, 0], - b = [5, 0], - c = [8, 4], - nodes = [ - iD.Node({loc: a}), - iD.Node({loc: b}) - ]; - - var choice = iD.geoChooseEdge(nodes, c, projection); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(5); - expect(choice.loc).to.eql([5, 0]); - }); - }); - - describe('geoLineIntersection', function() { - it('returns null if lines are colinear with overlap', function() { - var a = [[0, 0], [10, 0]], - b = [[-5, 0], [5, 0]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns null if lines are colinear but disjoint', function() { - var a = [[5, 0], [10, 0]], - b = [[-10, 0], [-5, 0]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns null if lines are parallel', function() { - var a = [[0, 0], [10, 0]], - b = [[0, 5], [10, 5]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns the intersection point between 2 lines', function() { - var a = [[0, 0], [10, 0]], - b = [[5, 10], [5, -10]]; - expect(iD.geoLineIntersection(a, b)).to.eql([5, 0]); - }); - it('returns null if lines are not parallel but not intersecting', function() { - var a = [[0, 0], [10, 0]], - b = [[-5, 10], [-5, -10]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - }); - - describe('geoPointInPolygon', function() { - it('says a point in a polygon is on a polygon', function() { - var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; - var point = [0.5, 0.5]; - expect(iD.geoPointInPolygon(point, poly)).to.be.true; - }); - it('says a point outside of a polygon is outside', function() { - var poly = [ - [0, 0], - [0, 1], - [1, 1], - [1, 0], - [0, 0]]; - var point = [0.5, 1.5]; - expect(iD.geoPointInPolygon(point, poly)).to.be.false; - }); - }); - - describe('geoPolygonContainsPolygon', function() { - it('says a polygon in a polygon is in', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.true; - }); - it('says a polygon outside of a polygon is out', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('geoPolygonIntersectsPolygon', function() { - it('returns true when outer polygon fully contains inner', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; - }); - - it('returns true when outer polygon partially contains inner (some vertices contained)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; - }); - - it('returns false when outer polygon partially contains inner (no vertices contained - lax test)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; - }); - - it('returns true when outer polygon partially contains inner (no vertices contained - strict test)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner, true)).to.be.true; - }); - - it('returns false when outer and inner are fully disjoint', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('geoPathLength', function() { - it('calculates a simple path length', function() { - var path = [[0, 0], [0, 1], [3, 5]]; - expect(iD.geoPathLength(path)).to.eql(6); - }); - - it('does not fail on single-point path', function() { - var path = [[0, 0]]; - expect(iD.geoPathLength(path)).to.eql(0); - }); - - it('estimates zero-length edges', function() { - var path = [[0, 0], [0, 0]]; - expect(iD.geoPathLength(path)).to.eql(0); - }); - }); }); diff --git a/test/spec/geo/geom.js b/test/spec/geo/geom.js new file mode 100644 index 000000000..188c738fd --- /dev/null +++ b/test/spec/geo/geom.js @@ -0,0 +1,424 @@ +describe('iD.geo - geometry', function() { + + describe('geoAngle', function() { + it('returns angle between a and b', function() { + var projection = function (_) { return _; }; + expect(iD.geoAngle({loc:[0, 0]}, {loc:[1, 0]}, projection)).to.be.closeTo(0, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, 1]}, projection)).to.be.closeTo(Math.PI / 2, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[-1, 0]}, projection)).to.be.closeTo(Math.PI, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, -1]}, projection)).to.be.closeTo(-Math.PI / 2, 1e-6); + }); + }); + + describe('geoEdgeEqual', function() { + it('returns false for inequal edges', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'c'])).to.be.false; + }); + + it('returns true for equal edges along same direction', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'b'])).to.be.true; + }); + + it('returns true for equal edges along opposite direction', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['b', 'a'])).to.be.true; + }); + }); + + describe('geoRotate', function() { + it('rotates points around [0, 0]', function() { + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [0, 0]; + var result = iD.geoRotate(points, angle, around); + expect(result[0][0]).to.be.closeTo(-5, 1e-6); + expect(result[0][1]).to.be.closeTo(0, 1e-6); + expect(result[1][0]).to.be.closeTo(-5, 1e-6); + expect(result[1][1]).to.be.closeTo(-1, 1e-6); + }); + + it('rotates points around [3, 0]', function() { + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [3, 0]; + var result = iD.geoRotate(points, angle, around); + expect(result[0][0]).to.be.closeTo(1, 1e-6); + expect(result[0][1]).to.be.closeTo(0, 1e-6); + expect(result[1][0]).to.be.closeTo(1, 1e-6); + expect(result[1][1]).to.be.closeTo(-1, 1e-6); + }); + }); + + describe('geoChooseEdge', function() { + var projection = function (l) { return l; }; + projection.invert = projection; + + it('returns null for a degenerate way (no nodes)', function() { + expect(iD.geoChooseEdge([], [0, 0], projection)).to.be.null; + }); + + it('returns null for a degenerate way (single node)', function() { + expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.be.null; + }); + + it('calculates the orthogonal projection of a point onto a segment', function() { + // a --*--- b + // | + // c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [2, 1]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(nodes, c, projection); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('returns the starting vertex when the orthogonal projection is < 0', function() { + var a = [0, 0]; + var b = [5, 0]; + var c = [-3, 4]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(nodes, c, projection); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(5); + expect(choice.loc).to.eql([0, 0]); + }); + + it('returns the ending vertex when the orthogonal projection is > 1', function() { + var a = [0, 0]; + var b = [5, 0]; + var c = [8, 4]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(nodes, c, projection); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(5); + expect(choice.loc).to.eql([5, 0]); + }); + + it('skips the given nodeID at end of way', function() { + // + // a --*-- b + // e | + // | | + // d - c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 5]; + var e = [2, 0.1]; // e.g. user is dragging e onto ab + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, e, projection, 'e'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('skips the given nodeID in middle of way', function() { + // + // a --*-- b + // d | + // / \ | + // e c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 0.1]; // e.g. user is dragging d onto ab + var e = [0, 5]; + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, d, projection, 'd'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('returns null if all nodes are skipped', function() { + var nodes = [ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [5, 0]}), + ]; + var choice = iD.geoChooseEdge(nodes, [2, 2], projection, 'a'); + expect(choice).to.be.null; + }); + }); + + describe('geoHasSelfIntersections', function() { + it('returns false for a degenerate way (no nodes)', function() { + expect(iD.geoHasSelfIntersections([], '')).to.be.false; + }); + + it('returns false if no activeID', function() { + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 2]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d, a]; + expect(iD.geoHasSelfIntersections(nodes, '')).to.be.false; + }); + + it('returns false if there are no self intersections (closed way)', function() { + // a --- b + // | | + // | | + // d --- c + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 2]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d, a]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + }); + + it('returns true if there are self intersections without a junction (closed way)', function() { + // a c + // | \ / | + // | / | + // | / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d, a]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.true; + }); + + it('returns false if there are self intersections with a junction (closed way)', function() { + // a c + // | \ / | + // | x | + // | / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var x = iD.osmNode({id: 'x', loc: [1, 1]}); + var nodes = [a, x, b, c, x, d, a]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'x')).to.be.false; + }); + + it('returns false if there are no self intersections (open way)', function() { + // a --- b + // | + // | + // d --- c + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 2]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + }); + + it('returns true if there are self intersections without a junction (open way)', function() { + // a c + // \ / | + // / | + // / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.true; + }); + + it('returns false if there are self intersections with a junction (open way)', function() { + // a c + // \ / | + // x | + // / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var x = iD.osmNode({id: 'x', loc: [1, 1]}); + var nodes = [a, x, b, c, x, d]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'x')).to.be.false; + }); + + }); + + + describe('geoLineIntersection', function() { + it('returns null if lines are colinear with overlap', function() { + var a = [[0, 0], [10, 0]]; + var b = [[-5, 0], [5, 0]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns null if lines are colinear but disjoint', function() { + var a = [[5, 0], [10, 0]]; + var b = [[-10, 0], [-5, 0]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns null if lines are parallel', function() { + var a = [[0, 0], [10, 0]]; + var b = [[0, 5], [10, 5]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns the intersection point between 2 lines', function() { + var a = [[0, 0], [10, 0]]; + var b = [[5, 10], [5, -10]]; + expect(iD.geoLineIntersection(a, b)).to.eql([5, 0]); + }); + it('returns null if lines are not parallel but not intersecting', function() { + var a = [[0, 0], [10, 0]]; + var b = [[-5, 10], [-5, -10]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + }); + + describe('geoPointInPolygon', function() { + it('says a point in a polygon is on a polygon', function() { + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + var point = [0.5, 0.5]; + expect(iD.geoPointInPolygon(point, poly)).to.be.true; + }); + it('says a point outside of a polygon is outside', function() { + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + var point = [0.5, 1.5]; + expect(iD.geoPointInPolygon(point, poly)).to.be.false; + }); + }); + + describe('geoPolygonContainsPolygon', function() { + it('says a polygon in a polygon is in', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.true; + }); + it('says a polygon outside of a polygon is out', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('geoPolygonIntersectsPolygon', function() { + it('returns true when outer polygon fully contains inner', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('returns false when inner polygon fully contains outer', function() { + var inner = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var outer = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + + it('returns true when outer polygon partially contains inner (some vertices contained)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('returns false when outer polygon partially contains inner (no vertices contained - lax test)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + + it('returns true when outer polygon partially contains inner (no vertices contained - strict test)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner, true)).to.be.true; + }); + + it('returns false when outer and inner are fully disjoint', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('geoPathLength', function() { + it('calculates a simple path length', function() { + var path = [[0, 0], [0, 1], [3, 5]]; + expect(iD.geoPathLength(path)).to.eql(6); + }); + + it('does not fail on single-point path', function() { + var path = [[0, 0]]; + expect(iD.geoPathLength(path)).to.eql(0); + }); + + it('estimates zero-length edges', function() { + var path = [[0, 0], [0, 0]]; + expect(iD.geoPathLength(path)).to.eql(0); + }); + }); + + describe('geoViewportEdge', function() { + var dimensions = [1000, 1000]; + it('returns null if the point is not at the edge', function() { + expect(iD.geoViewportEdge([500, 500], dimensions)).to.be.null; + }); + it('nudges top edge', function() { + expect(iD.geoViewportEdge([500, 5], dimensions)).to.eql([0, 10]); + }); + it('nudges top-right corner', function() { + expect(iD.geoViewportEdge([995, 5], dimensions)).to.eql([-10, 10]); + }); + it('nudges right edge', function() { + expect(iD.geoViewportEdge([995, 500], dimensions)).to.eql([-10, 0]); + }); + it('nudges bottom-right corner', function() { + expect(iD.geoViewportEdge([995, 995], dimensions)).to.eql([-10, -10]); + }); + it('nudges bottom edge', function() { + expect(iD.geoViewportEdge([500, 995], dimensions)).to.eql([0, -10]); + }); + it('nudges bottom-left corner', function() { + expect(iD.geoViewportEdge([5, 995], dimensions)).to.eql([10, -10]); + }); + it('nudges left edge', function() { + expect(iD.geoViewportEdge([5, 500], dimensions)).to.eql([10, 0]); + }); + it('nudges top-left corner', function() { + expect(iD.geoViewportEdge([5, 5], dimensions)).to.eql([10, 10]); + }); + }); + +}); diff --git a/test/spec/geo/vector.js b/test/spec/geo/vector.js new file mode 100644 index 000000000..7b69aab00 --- /dev/null +++ b/test/spec/geo/vector.js @@ -0,0 +1,115 @@ +describe('iD.geo - vector', function() { + + describe('geoVecEqual', function() { + it('tests vectors for equality', function() { + expect(iD.geoVecEqual([1, 2], [1, 2])).to.be.true; + expect(iD.geoVecEqual([1, 2], [1, 0])).to.be.false; + expect(iD.geoVecEqual([1, 2], [2, 1])).to.be.false; + }); + }); + + describe('geoVecAdd', function() { + it('adds vectors', function() { + expect(iD.geoVecAdd([1, 2], [3, 4])).to.eql([4, 6]); + expect(iD.geoVecAdd([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecAdd([1, 2], [-3, -4])).to.eql([-2, -2]); + }); + }); + + describe('geoVecSubtract', function() { + it('subtracts vectors', function() { + expect(iD.geoVecSubtract([1, 2], [3, 4])).to.eql([-2, -2]); + expect(iD.geoVecSubtract([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecSubtract([1, 2], [-3, -4])).to.eql([4, 6]); + }); + }); + + describe('geoVecScale', function() { + it('multiplies vectors', function() { + expect(iD.geoVecScale([1, 2], 0)).to.eql([0, 0]); + expect(iD.geoVecScale([1, 2], 1)).to.eql([1, 2]); + expect(iD.geoVecScale([1, 2], 2)).to.eql([2, 4]); + expect(iD.geoVecScale([1, 2], 0.5)).to.eql([0.5, 1]); + }); + }); + + describe('geoVecFloor (was: geoRoundCoordinates)', function() { + it('rounds vectors', function() { + expect(iD.geoVecFloor([0.1, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1.1])).to.eql([0, 1]); + }); + }); + + describe('geoVecInterp', function() { + it('interpolates halfway', function() { + var a = [0, 0]; + var b = [10, 10]; + expect(iD.geoVecInterp(a, b, 0.5)).to.eql([5, 5]); + }); + it('interpolates to one side', function() { + var a = [0, 0]; + var b = [10, 10]; + expect(iD.geoVecInterp(a, b, 0)).to.eql([0, 0]); + }); + }); + + describe('geoVecLength (was: geoEuclideanDistance)', function() { + it('distance between two same points is zero', function() { + var a = [0, 0]; + var b = [0, 0]; + expect(iD.geoVecLength(a, b)).to.eql(0); + }); + it('a straight 10 unit line is 10', function() { + var a = [0, 0]; + var b = [10, 0]; + expect(iD.geoVecLength(a, b)).to.eql(10); + }); + it('a pythagorean triangle is right', function() { + var a = [0, 0]; + var b = [4, 3]; + expect(iD.geoVecLength(a, b)).to.eql(5); + }); + }); + + describe('geoVecAngle', function() { + it('returns angle between a and b', function() { + expect(iD.geoVecAngle([0, 0], [1, 0])).to.be.closeTo(0, 1e-6); + expect(iD.geoVecAngle([0, 0], [0, 1])).to.be.closeTo(Math.PI / 2, 1e-6); + expect(iD.geoVecAngle([0, 0], [-1, 0])).to.be.closeTo(Math.PI, 1e-6); + expect(iD.geoVecAngle([0, 0], [0, -1])).to.be.closeTo(-Math.PI / 2, 1e-6); + }); + }); + + describe('geoVecDot', function() { + it('dot product of right angle is zero', function() { + var a = [1, 0]; + var b = [0, 1]; + expect(iD.geoVecDot(a, b)).to.eql(0); + }); + it('dot product of same vector multiplies', function() { + var a = [2, 0]; + var b = [2, 0]; + expect(iD.geoVecDot(a, b)).to.eql(4); + }); + }); + + describe('geoVecCross', function() { + it('2D cross product of right hand turn is positive', function() { + var a = [2, 0]; + var b = [0, 2]; + expect(iD.geoVecCross(a, b)).to.eql(4); + }); + it('2D cross product of left hand turn is negative', function() { + var a = [2, 0]; + var b = [0, -2]; + expect(iD.geoVecCross(a, b)).to.eql(-4); + }); + it('2D cross product of colinear points is zero', function() { + var a = [-2, 0]; + var b = [2, 0]; + expect(iD.geoVecCross(a, b)).to.equal(0); + }); + }); + +}); diff --git a/test/spec/osm/node.js b/test/spec/osm/node.js index 53de624dc..dd005c889 100644 --- a/test/spec/osm/node.js +++ b/test/spec/osm/node.js @@ -1,66 +1,66 @@ describe('iD.osmNode', function () { it('returns a node', function () { - expect(iD.Node()).to.be.an.instanceOf(iD.Node); - expect(iD.Node().type).to.equal('node'); + expect(iD.osmNode()).to.be.an.instanceOf(iD.osmNode); + expect(iD.osmNode().type).to.equal('node'); }); it('defaults tags to an empty object', function () { - expect(iD.Node().tags).to.eql({}); + expect(iD.osmNode().tags).to.eql({}); }); it('sets tags as specified', function () { - expect(iD.Node({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'}); + expect(iD.osmNode({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'}); }); describe('#extent', function() { it('returns a point extent', function() { - expect(iD.Node({loc: [5, 10]}).extent().equals([[5, 10], [5, 10]])).to.be.ok; + expect(iD.osmNode({loc: [5, 10]}).extent().equals([[5, 10], [5, 10]])).to.be.ok; }); }); describe('#intersects', function () { it('returns true for a node within the given extent', function () { - expect(iD.Node({loc: [0, 0]}).intersects([[-5, -5], [5, 5]])).to.equal(true); + expect(iD.osmNode({loc: [0, 0]}).intersects([[-5, -5], [5, 5]])).to.equal(true); }); it('returns false for a node outside the given extend', function () { - expect(iD.Node({loc: [6, 6]}).intersects([[-5, -5], [5, 5]])).to.equal(false); + expect(iD.osmNode({loc: [6, 6]}).intersects([[-5, -5], [5, 5]])).to.equal(false); }); }); describe('#geometry', function () { it('returns \'vertex\' if the node is a member of any way', function () { - var node = iD.Node(), - way = iD.Way({nodes: [node.id]}), - graph = iD.Graph([node, way]); + var node = iD.osmNode(), + way = iD.osmWay({nodes: [node.id]}), + graph = iD.coreGraph([node, way]); expect(node.geometry(graph)).to.equal('vertex'); }); it('returns \'point\' if the node is not a member of any way', function () { - var node = iD.Node(), - graph = iD.Graph([node]); + var node = iD.osmNode(), + graph = iD.coreGraph([node]); expect(node.geometry(graph)).to.equal('point'); }); }); describe('#isEndpoint', function () { it('returns true for a node at an endpoint along a linear way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c']}), + graph = iD.coreGraph([a, b, c, w]); expect(a.isEndpoint(graph)).to.equal(true, 'linear way, beginning node'); expect(b.isEndpoint(graph)).to.equal(false, 'linear way, middle node'); expect(c.isEndpoint(graph)).to.equal(true, 'linear way, ending node'); }); it('returns false for nodes along a circular way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c', 'a']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c', 'a']}), + graph = iD.coreGraph([a, b, c, w]); expect(a.isEndpoint(graph)).to.equal(false, 'circular way, connector node'); expect(b.isEndpoint(graph)).to.equal(false, 'circular way, middle node'); expect(c.isEndpoint(graph)).to.equal(false, 'circular way, ending node'); @@ -69,120 +69,507 @@ describe('iD.osmNode', function () { describe('#isConnected', function () { it('returns true for a node with multiple parent ways, at least one interesting', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id]}), - w2 = iD.Way({nodes: [node.id], tags: { highway: 'residential' }}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id]}), + w2 = iD.osmWay({nodes: [node.id], tags: { highway: 'residential' }}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isConnected(graph)).to.equal(true); }); it('returns false for a node with only area parent ways', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: { area: 'yes' }}), - w2 = iD.Way({nodes: [node.id], tags: { area: 'yes' }}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: { area: 'yes' }}), + w2 = iD.osmWay({nodes: [node.id], tags: { area: 'yes' }}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isConnected(graph)).to.equal(false); }); it('returns false for a node with only uninteresting parent ways', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id]}), - w2 = iD.Way({nodes: [node.id]}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id]}), + w2 = iD.osmWay({nodes: [node.id]}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isConnected(graph)).to.equal(false); }); it('returns false for a standalone node on a single parent way', function () { - var node = iD.Node(), - way = iD.Way({nodes: [node.id]}), - graph = iD.Graph([node, way]); + var node = iD.osmNode(), + way = iD.osmWay({nodes: [node.id]}), + graph = iD.coreGraph([node, way]); expect(node.isConnected(graph)).to.equal(false); }); it('returns true for a self-intersecting node on a single parent way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c', 'b']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c', 'b']}), + graph = iD.coreGraph([a, b, c, w]); expect(b.isConnected(graph)).to.equal(true); }); it('returns false for the connecting node of a closed way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c', 'a']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c', 'a']}), + graph = iD.coreGraph([a, b, c, w]); expect(a.isConnected(graph)).to.equal(false); }); }); describe('#isIntersection', function () { it('returns true for a node shared by more than one highway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - w2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isIntersection(graph)).to.equal(true); }); it('returns true for a node shared by more than one waterway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - w2 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isIntersection(graph)).to.equal(true); }); }); describe('#isHighwayIntersection', function () { it('returns true for a node shared by more than one highway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - w2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isHighwayIntersection(graph)).to.equal(true); }); it('returns false for a node shared by more than one waterway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - w2 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isHighwayIntersection(graph)).to.equal(false); }); }); describe('#isDegenerate', function () { it('returns true if node has invalid loc', function () { - expect(iD.Node().isDegenerate()).to.be.equal(true, 'no loc'); - expect(iD.Node({loc: ''}).isDegenerate()).to.be.equal(true, 'empty string loc'); - expect(iD.Node({loc: []}).isDegenerate()).to.be.equal(true, 'empty array loc'); - expect(iD.Node({loc: [0]}).isDegenerate()).to.be.equal(true, '1-array loc'); - expect(iD.Node({loc: [0, 0, 0]}).isDegenerate()).to.be.equal(true, '3-array loc'); - expect(iD.Node({loc: [-181, 0]}).isDegenerate()).to.be.equal(true, '< min lon'); - expect(iD.Node({loc: [181, 0]}).isDegenerate()).to.be.equal(true, '> max lon'); - expect(iD.Node({loc: [0, -91]}).isDegenerate()).to.be.equal(true, '< min lat'); - expect(iD.Node({loc: [0, 91]}).isDegenerate()).to.be.equal(true, '> max lat'); - expect(iD.Node({loc: [Infinity, 0]}).isDegenerate()).to.be.equal(true, 'Infinity lon'); - expect(iD.Node({loc: [0, Infinity]}).isDegenerate()).to.be.equal(true, 'Infinity lat'); - expect(iD.Node({loc: [NaN, 0]}).isDegenerate()).to.be.equal(true, 'NaN lon'); - expect(iD.Node({loc: [0, NaN]}).isDegenerate()).to.be.equal(true, 'NaN lat'); + expect(iD.osmNode().isDegenerate()).to.be.equal(true, 'no loc'); + expect(iD.osmNode({loc: ''}).isDegenerate()).to.be.equal(true, 'empty string loc'); + expect(iD.osmNode({loc: []}).isDegenerate()).to.be.equal(true, 'empty array loc'); + expect(iD.osmNode({loc: [0]}).isDegenerate()).to.be.equal(true, '1-array loc'); + expect(iD.osmNode({loc: [0, 0, 0]}).isDegenerate()).to.be.equal(true, '3-array loc'); + expect(iD.osmNode({loc: [-181, 0]}).isDegenerate()).to.be.equal(true, '< min lon'); + expect(iD.osmNode({loc: [181, 0]}).isDegenerate()).to.be.equal(true, '> max lon'); + expect(iD.osmNode({loc: [0, -91]}).isDegenerate()).to.be.equal(true, '< min lat'); + expect(iD.osmNode({loc: [0, 91]}).isDegenerate()).to.be.equal(true, '> max lat'); + expect(iD.osmNode({loc: [Infinity, 0]}).isDegenerate()).to.be.equal(true, 'Infinity lon'); + expect(iD.osmNode({loc: [0, Infinity]}).isDegenerate()).to.be.equal(true, 'Infinity lat'); + expect(iD.osmNode({loc: [NaN, 0]}).isDegenerate()).to.be.equal(true, 'NaN lon'); + expect(iD.osmNode({loc: [0, NaN]}).isDegenerate()).to.be.equal(true, 'NaN lat'); }); it('returns false if node has valid loc', function () { - expect(iD.Node({loc: [0, 0]}).isDegenerate()).to.be.equal(false, '2-array loc'); - expect(iD.Node({loc: [-180, 0]}).isDegenerate()).to.be.equal(false, 'min lon'); - expect(iD.Node({loc: [180, 0]}).isDegenerate()).to.be.equal(false, 'max lon'); - expect(iD.Node({loc: [0, -90]}).isDegenerate()).to.be.equal(false, 'min lat'); - expect(iD.Node({loc: [0, 90]}).isDegenerate()).to.be.equal(false, 'max lat'); + expect(iD.osmNode({loc: [0, 0]}).isDegenerate()).to.be.equal(false, '2-array loc'); + expect(iD.osmNode({loc: [-180, 0]}).isDegenerate()).to.be.equal(false, 'min lon'); + expect(iD.osmNode({loc: [180, 0]}).isDegenerate()).to.be.equal(false, 'max lon'); + expect(iD.osmNode({loc: [0, -90]}).isDegenerate()).to.be.equal(false, 'min lat'); + expect(iD.osmNode({loc: [0, 90]}).isDegenerate()).to.be.equal(false, 'max lat'); }); }); + describe('#directions', function () { + var projection = function (_) { return _; }; + it('returns empty array if no direction tag', function () { + var node1 = iD.osmNode({ loc: [0, 0], tags: {}}); + var graph = iD.coreGraph([node1]); + expect(node1.directions(graph, projection)).to.eql([], 'no direction tag'); + }); + + it('returns empty array if nonsense direction tag', function () { + var node1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'blah' }}); + var node2 = iD.osmNode({ loc: [0, 0], tags: { direction: '' }}); + var node3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NaN' }}); + var node4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'eastwest' }}); + var graph = iD.coreGraph([node1, node2, node3, node4]); + + expect(node1.directions(graph, projection)).to.eql([], 'nonsense direction tag'); + expect(node2.directions(graph, projection)).to.eql([], 'empty string direction tag'); + expect(node3.directions(graph, projection)).to.eql([], 'NaN direction tag'); + expect(node4.directions(graph, projection)).to.eql([], 'eastwest direction tag'); + }); + + it('supports numeric direction tag', function () { + var node1 = iD.osmNode({ loc: [0, 0], tags: { direction: '0' }}); + var node2 = iD.osmNode({ loc: [0, 0], tags: { direction: '45' }}); + var node3 = iD.osmNode({ loc: [0, 0], tags: { direction: '-45' }}); + var node4 = iD.osmNode({ loc: [0, 0], tags: { direction: '360' }}); + var node5 = iD.osmNode({ loc: [0, 0], tags: { direction: '1000' }}); + var graph = iD.coreGraph([node1, node2, node3, node4, node5]); + + expect(node1.directions(graph, projection)).to.eql([0], 'numeric 0'); + expect(node2.directions(graph, projection)).to.eql([45], 'numeric 45'); + expect(node3.directions(graph, projection)).to.eql([-45], 'numeric -45'); + expect(node4.directions(graph, projection)).to.eql([360], 'numeric 360'); + expect(node5.directions(graph, projection)).to.eql([1000], 'numeric 1000'); + }); + + it('supports cardinal direction tags (test abbreviated and mixed case)', function () { + var nodeN1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'n' }}); + var nodeN2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'N' }}); + var nodeN3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'north' }}); + var nodeN4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NOrth' }}); + + var nodeNNE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nne' }}); + var nodeNNE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NnE' }}); + var nodeNNE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northnortheast' }}); + var nodeNNE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NOrthnorTHEast' }}); + + var nodeNE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ne' }}); + var nodeNE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nE' }}); + var nodeNE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northeast' }}); + var nodeNE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'norTHEast' }}); + + var nodeENE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ene' }}); + var nodeENE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EnE' }}); + var nodeENE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'eastnortheast' }}); + var nodeENE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EAstnorTHEast' }}); + + var nodeE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'e' }}); + var nodeE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'E' }}); + var nodeE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'east' }}); + var nodeE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EAst' }}); + + var nodeESE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ese' }}); + var nodeESE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EsE' }}); + var nodeESE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'eastsoutheast' }}); + var nodeESE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EAstsouTHEast' }}); + + var nodeSE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'se' }}); + var nodeSE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sE' }}); + var nodeSE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southeast' }}); + var nodeSE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'souTHEast' }}); + + var nodeSSE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sse' }}); + var nodeSSE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SsE' }}); + var nodeSSE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southsoutheast' }}); + var nodeSSE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SOuthsouTHEast' }}); + + var nodeS1 = iD.osmNode({ loc: [0, 0], tags: { direction: 's' }}); + var nodeS2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'S' }}); + var nodeS3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'south' }}); + var nodeS4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SOuth' }}); + + var nodeSSW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ssw' }}); + var nodeSSW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SsW' }}); + var nodeSSW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southsouthwest' }}); + var nodeSSW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SOuthsouTHWest' }}); + + var nodeSW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sw' }}); + var nodeSW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sW' }}); + var nodeSW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southwest' }}); + var nodeSW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'souTHWest' }}); + + var nodeWSW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'wsw' }}); + var nodeWSW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WsW' }}); + var nodeWSW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'westsouthwest' }}); + var nodeWSW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WEstsouTHWest' }}); + + var nodeW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'w' }}); + var nodeW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'W' }}); + var nodeW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'west' }}); + var nodeW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WEst' }}); + + var nodeWNW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'wnw' }}); + var nodeWNW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WnW' }}); + var nodeWNW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'westnorthwest' }}); + var nodeWNW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WEstnorTHWest' }}); + + var nodeNW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nw' }}); + var nodeNW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nW' }}); + var nodeNW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northwest' }}); + var nodeNW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'norTHWest' }}); + + var nodeNNW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nnw' }}); + var nodeNNW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NnW' }}); + var nodeNNW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northnorthwest' }}); + var nodeNNW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NOrthnorTHWest' }}); + + var graph = iD.coreGraph([ + nodeN1, nodeN2, nodeN3, nodeN4, + nodeNNE1, nodeNNE2, nodeNNE3, nodeNNE4, + nodeNE1, nodeNE2, nodeNE3, nodeNE4, + nodeENE1, nodeENE2, nodeENE3, nodeENE4, + nodeE1, nodeE2, nodeE3, nodeE4, + nodeESE1, nodeESE2, nodeESE3, nodeESE4, + nodeSE1, nodeSE2, nodeSE3, nodeSE4, + nodeSSE1, nodeSSE2, nodeSSE3, nodeSSE4, + nodeS1, nodeS2, nodeS3, nodeS4, + nodeSSW1, nodeSSW2, nodeSSW3, nodeSSW4, + nodeSW1, nodeSW2, nodeSW3, nodeSW4, + nodeWSW1, nodeWSW2, nodeWSW3, nodeWSW4, + nodeW1, nodeW2, nodeW3, nodeW4, + nodeWNW1, nodeWNW2, nodeWNW3, nodeWNW4, + nodeNW1, nodeNW2, nodeNW3, nodeNW4, + nodeNNW1, nodeNNW2, nodeNNW3, nodeNNW4 + ]); + + expect(nodeN1.directions(graph, projection)).to.eql([0], 'cardinal n'); + expect(nodeN2.directions(graph, projection)).to.eql([0], 'cardinal N'); + expect(nodeN3.directions(graph, projection)).to.eql([0], 'cardinal north'); + expect(nodeN4.directions(graph, projection)).to.eql([0], 'cardinal NOrth'); + + expect(nodeNNE1.directions(graph, projection)).to.eql([22], 'cardinal nne'); + expect(nodeNNE2.directions(graph, projection)).to.eql([22], 'cardinal NnE'); + expect(nodeNNE3.directions(graph, projection)).to.eql([22], 'cardinal northnortheast'); + expect(nodeNNE4.directions(graph, projection)).to.eql([22], 'cardinal NOrthnorTHEast'); + + expect(nodeNE1.directions(graph, projection)).to.eql([45], 'cardinal ne'); + expect(nodeNE2.directions(graph, projection)).to.eql([45], 'cardinal nE'); + expect(nodeNE3.directions(graph, projection)).to.eql([45], 'cardinal northeast'); + expect(nodeNE4.directions(graph, projection)).to.eql([45], 'cardinal norTHEast'); + + expect(nodeENE1.directions(graph, projection)).to.eql([67], 'cardinal ene'); + expect(nodeENE2.directions(graph, projection)).to.eql([67], 'cardinal EnE'); + expect(nodeENE3.directions(graph, projection)).to.eql([67], 'cardinal eastnortheast'); + expect(nodeENE4.directions(graph, projection)).to.eql([67], 'cardinal EAstnorTHEast'); + + expect(nodeE1.directions(graph, projection)).to.eql([90], 'cardinal e'); + expect(nodeE2.directions(graph, projection)).to.eql([90], 'cardinal E'); + expect(nodeE3.directions(graph, projection)).to.eql([90], 'cardinal east'); + expect(nodeE4.directions(graph, projection)).to.eql([90], 'cardinal EAst'); + + expect(nodeESE1.directions(graph, projection)).to.eql([112], 'cardinal ese'); + expect(nodeESE2.directions(graph, projection)).to.eql([112], 'cardinal EsE'); + expect(nodeESE3.directions(graph, projection)).to.eql([112], 'cardinal eastsoutheast'); + expect(nodeESE4.directions(graph, projection)).to.eql([112], 'cardinal EAstsouTHEast'); + + expect(nodeSE1.directions(graph, projection)).to.eql([135], 'cardinal se'); + expect(nodeSE2.directions(graph, projection)).to.eql([135], 'cardinal sE'); + expect(nodeSE3.directions(graph, projection)).to.eql([135], 'cardinal southeast'); + expect(nodeSE4.directions(graph, projection)).to.eql([135], 'cardinal souTHEast'); + + expect(nodeSSE1.directions(graph, projection)).to.eql([157], 'cardinal sse'); + expect(nodeSSE2.directions(graph, projection)).to.eql([157], 'cardinal SsE'); + expect(nodeSSE3.directions(graph, projection)).to.eql([157], 'cardinal southsoutheast'); + expect(nodeSSE4.directions(graph, projection)).to.eql([157], 'cardinal SouthsouTHEast'); + + expect(nodeS1.directions(graph, projection)).to.eql([180], 'cardinal s'); + expect(nodeS2.directions(graph, projection)).to.eql([180], 'cardinal S'); + expect(nodeS3.directions(graph, projection)).to.eql([180], 'cardinal south'); + expect(nodeS4.directions(graph, projection)).to.eql([180], 'cardinal SOuth'); + + expect(nodeSSW1.directions(graph, projection)).to.eql([202], 'cardinal ssw'); + expect(nodeSSW2.directions(graph, projection)).to.eql([202], 'cardinal SsW'); + expect(nodeSSW3.directions(graph, projection)).to.eql([202], 'cardinal southsouthwest'); + expect(nodeSSW4.directions(graph, projection)).to.eql([202], 'cardinal SouthsouTHWest'); + + expect(nodeSW1.directions(graph, projection)).to.eql([225], 'cardinal sw'); + expect(nodeSW2.directions(graph, projection)).to.eql([225], 'cardinal sW'); + expect(nodeSW3.directions(graph, projection)).to.eql([225], 'cardinal southwest'); + expect(nodeSW4.directions(graph, projection)).to.eql([225], 'cardinal souTHWest'); + + expect(nodeWSW1.directions(graph, projection)).to.eql([247], 'cardinal wsw'); + expect(nodeWSW2.directions(graph, projection)).to.eql([247], 'cardinal WsW'); + expect(nodeWSW3.directions(graph, projection)).to.eql([247], 'cardinal westsouthwest'); + expect(nodeWSW4.directions(graph, projection)).to.eql([247], 'cardinal WEstsouTHWest'); + + expect(nodeW1.directions(graph, projection)).to.eql([270], 'cardinal w'); + expect(nodeW2.directions(graph, projection)).to.eql([270], 'cardinal W'); + expect(nodeW3.directions(graph, projection)).to.eql([270], 'cardinal west'); + expect(nodeW4.directions(graph, projection)).to.eql([270], 'cardinal WEst'); + + expect(nodeWNW1.directions(graph, projection)).to.eql([292], 'cardinal wnw'); + expect(nodeWNW2.directions(graph, projection)).to.eql([292], 'cardinal WnW'); + expect(nodeWNW3.directions(graph, projection)).to.eql([292], 'cardinal westnorthwest'); + expect(nodeWNW4.directions(graph, projection)).to.eql([292], 'cardinal WEstnorTHWest'); + + expect(nodeNW1.directions(graph, projection)).to.eql([315], 'cardinal nw'); + expect(nodeNW2.directions(graph, projection)).to.eql([315], 'cardinal nW'); + expect(nodeNW3.directions(graph, projection)).to.eql([315], 'cardinal northwest'); + expect(nodeNW4.directions(graph, projection)).to.eql([315], 'cardinal norTHWest'); + + expect(nodeNNW1.directions(graph, projection)).to.eql([337], 'cardinal nnw'); + expect(nodeNNW2.directions(graph, projection)).to.eql([337], 'cardinal NnW'); + expect(nodeNNW3.directions(graph, projection)).to.eql([337], 'cardinal northnorthwest'); + expect(nodeNNW4.directions(graph, projection)).to.eql([337], 'cardinal NOrthnorTHWest'); + }); + + it('supports direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports traffic_signals:direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports traffic_signals:direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports traffic_signals:direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports traffic_signals:direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports railway:signal:direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports railway:signal:direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports railway:signal:direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports railway:signal:direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports camera:direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports camera:direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports camera:direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports camera:direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('returns directions for an all-way stop at a highway interstction', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'highway': 'stop', 'stop': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var node4 = iD.osmNode({ id: 'n4', loc: [0, -1] }); + var node5 = iD.osmNode({ id: 'n5', loc: [0, 1] }); + var way1 = iD.osmWay({ id: 'w1', nodes: ['n1','n2','n3'], tags: { 'highway': 'residential' } }); + var way2 = iD.osmWay({ id: 'w2', nodes: ['n4','n2','n5'], tags: { 'highway': 'residential' } }); + var graph = iD.coreGraph([node1, node2, node3, node4, node5, way1, way2]); + expect(node2.directions(graph, projection)).to.have.members([0, 90, 180, 270]); + }); + + it('does not return directions for an all-way stop not at a highway interstction', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0], tags: { 'highway': 'stop', 'stop': 'all' } }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0] }); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0], tags: { 'highway': 'stop', 'stop': 'all' } }); + var node4 = iD.osmNode({ id: 'n4', loc: [0, -1], tags: { 'highway': 'stop', 'stop': 'all' } }); + var node5 = iD.osmNode({ id: 'n5', loc: [0, 1], tags: { 'highway': 'stop', 'stop': 'all' } }); + var way1 = iD.osmWay({ id: 'w1', nodes: ['n1','n2','n3'], tags: { 'highway': 'residential' } }); + var way2 = iD.osmWay({ id: 'w2', nodes: ['n4','n2','n5'], tags: { 'highway': 'residential' } }); + var graph = iD.coreGraph([node1, node2, node3, node4, node5, way1, way2]); + expect(node2.directions(graph, projection)).to.eql([]); + }); + + }); + describe('#asJXON', function () { it('converts a node to jxon', function() { - var node = iD.Node({id: 'n-1', loc: [-77, 38], tags: {amenity: 'cafe'}}); + var node = iD.osmNode({id: 'n-1', loc: [-77, 38], tags: {amenity: 'cafe'}}); expect(node.asJXON()).to.eql({node: { '@id': '-1', '@lon': -77, @@ -192,13 +579,13 @@ describe('iD.osmNode', function () { }); it('includes changeset if provided', function() { - expect(iD.Node({loc: [0, 0]}).asJXON('1234').node['@changeset']).to.equal('1234'); + expect(iD.osmNode({loc: [0, 0]}).asJXON('1234').node['@changeset']).to.equal('1234'); }); }); describe('#asGeoJSON', function () { it('converts to a GeoJSON Point geometry', function () { - var node = iD.Node({tags: {amenity: 'cafe'}, loc: [1, 2]}), + var node = iD.osmNode({tags: {amenity: 'cafe'}, loc: [1, 2]}), json = node.asGeoJSON(); expect(json.type).to.equal('Point'); diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 7397682bd..1b9e59e08 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -1,17 +1,18 @@ describe('iD.svgAreas', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]), - all = function() { return true; }, - none = function() { return false; }; + var context, surface; + var all = function() { return true; }; + var none = function() { return false; }; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(iD.geoZoomToScale(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); + beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); iD.setAreaKeys({ @@ -22,13 +23,13 @@ describe('iD.svgAreas', function () { }); it('adds way and area classes', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 0]}), - iD.Node({id: 'c', loc: [1, 1]}), - iD.Node({id: 'd', loc: [0, 1]}), - iD.Way({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0]}), + iD.osmNode({id: 'c', loc: [1, 1]}), + iD.osmNode({id: 'd', loc: [0, 1]}), + iD.osmWay({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) + ]); surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], none); @@ -37,13 +38,13 @@ describe('iD.svgAreas', function () { }); it('adds tag classes', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 0]}), - iD.Node({id: 'c', loc: [1, 1]}), - iD.Node({id: 'd', loc: [0, 1]}), - iD.Way({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0]}), + iD.osmNode({id: 'c', loc: [1, 1]}), + iD.osmNode({id: 'd', loc: [0, 1]}), + iD.osmWay({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) + ]); surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], none); @@ -52,14 +53,14 @@ describe('iD.svgAreas', function () { }); it('handles deletion of a way and a member vertex (#1903)', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 0]}), - iD.Node({id: 'c', loc: [1, 1]}), - iD.Node({id: 'd', loc: [1, 1]}), - iD.Way({id: 'w', tags: {area: 'yes'}, nodes: ['a', 'b', 'c', 'a']}), - iD.Way({id: 'x', tags: {area: 'yes'}, nodes: ['a', 'b', 'd', 'a']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0]}), + iD.osmNode({id: 'c', loc: [1, 1]}), + iD.osmNode({id: 'd', loc: [1, 1]}), + iD.osmWay({id: 'w', tags: {area: 'yes'}, nodes: ['a', 'b', 'c', 'a']}), + iD.osmWay({id: 'x', tags: {area: 'yes'}, nodes: ['a', 'b', 'd', 'a']}) + ]); surface.call(iD.svgAreas(projection, context), graph, [graph.entity('x')], all); graph = graph.remove(graph.entity('x')).remove(graph.entity('d')); @@ -69,18 +70,18 @@ describe('iD.svgAreas', function () { }); describe('z-indexing', function() { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [-0.0002, 0.0001]}), - iD.Node({id: 'b', loc: [ 0.0002, 0.0001]}), - iD.Node({id: 'c', loc: [ 0.0002, -0.0001]}), - iD.Node({id: 'd', loc: [-0.0002, -0.0001]}), - iD.Node({id: 'e', loc: [-0.0004, 0.0002]}), - iD.Node({id: 'f', loc: [ 0.0004, 0.0002]}), - iD.Node({id: 'g', loc: [ 0.0004, -0.0002]}), - iD.Node({id: 'h', loc: [-0.0004, -0.0002]}), - iD.Way({id: 's', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}), - iD.Way({id: 'l', tags: {landuse: 'park'}, nodes: ['e', 'f', 'g', 'h', 'e']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [-0.0002, 0.0001]}), + iD.osmNode({id: 'b', loc: [ 0.0002, 0.0001]}), + iD.osmNode({id: 'c', loc: [ 0.0002, -0.0001]}), + iD.osmNode({id: 'd', loc: [-0.0002, -0.0001]}), + iD.osmNode({id: 'e', loc: [-0.0004, 0.0002]}), + iD.osmNode({id: 'f', loc: [ 0.0004, 0.0002]}), + iD.osmNode({id: 'g', loc: [ 0.0004, -0.0002]}), + iD.osmNode({id: 'h', loc: [-0.0004, -0.0002]}), + iD.osmWay({id: 's', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}), + iD.osmWay({id: 'l', tags: {landuse: 'park'}, nodes: ['e', 'f', 'g', 'h', 'e']}) + ]); it('stacks smaller areas above larger ones in a single render', function () { surface.call(iD.svgAreas(projection, context), graph, [graph.entity('s'), graph.entity('l')], none); @@ -114,13 +115,13 @@ describe('iD.svgAreas', function () { }); it('renders fills for multipolygon areas', function () { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), - graph = iD.Graph([a, b, c, w, r]), - areas = [w, r]; + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}); + var graph = iD.coreGraph([a, b, c, w, r]); + var areas = [w, r]; surface.call(iD.svgAreas(projection, context), graph, areas, none); @@ -128,13 +129,13 @@ describe('iD.svgAreas', function () { }); it('renders no strokes for multipolygon areas', function () { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), - graph = iD.Graph([a, b, c, w, r]), - areas = [w, r]; + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}); + var graph = iD.coreGraph([a, b, c, w, r]); + var areas = [w, r]; surface.call(iD.svgAreas(projection, context), graph, areas, none); @@ -142,12 +143,12 @@ describe('iD.svgAreas', function () { }); it('renders fill for a multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, w, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, w, r]); surface.call(iD.svgAreas(projection, context), graph, [w, r], none); @@ -157,12 +158,12 @@ describe('iD.svgAreas', function () { }); it('renders no strokes for a multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, w, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, w, r]); surface.call(iD.svgAreas(projection, context), graph, [w, r], none); diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index e3cb29a89..75c957daa 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -1,12 +1,12 @@ describe('iD.svgLayers', function () { - var context, container, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]); + var context, container; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(iD.geoZoomToScale(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); container = d3.select(document.createElement('div')); }); diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index c1748eeff..d10fe09a6 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -1,26 +1,27 @@ describe('iD.svgLines', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]), - all = function() { return true; }, - none = function() { return false; }; + var context, surface; + var all = function() { return true; }; + var none = function() { return false; }; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(iD.geoZoomToScale(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); + beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); }); it('adds way and line classes', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [1, 1]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 1]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); surface.call(iD.svgLines(projection, context), graph, [line], all); @@ -29,10 +30,10 @@ describe('iD.svgLines', function () { }); it('adds tag classes', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [1, 1]}), - line = iD.Way({nodes: [a.id, b.id], tags: {highway: 'residential'}}), - graph = iD.Graph([a, b, line]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 1]}); + var line = iD.osmWay({nodes: [a.id, b.id], tags: {highway: 'residential'}}); + var graph = iD.coreGraph([a, b, line]); surface.call(iD.svgLines(projection, context), graph, [line], all); @@ -41,11 +42,11 @@ describe('iD.svgLines', function () { }); it('adds stroke classes for the tags of the parent relation of multipolygon members', function() { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [1, 1]}), - line = iD.Way({nodes: [a.id, b.id]}), - relation = iD.Relation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}), - graph = iD.Graph([a, b, line, relation]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 1]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var relation = iD.osmRelation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}); + var graph = iD.coreGraph([a, b, line, relation]); surface.call(iD.svgLines(projection, context), graph, [line], all); @@ -53,12 +54,12 @@ describe('iD.svgLines', function () { }); it('renders stroke for outer way of multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({id: 'w-1', tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: w.id}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, w, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({id: 'w-1', tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: w.id}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, w, r]); surface.call(iD.svgLines(projection, context), graph, [w], all); @@ -67,13 +68,13 @@ describe('iD.svgLines', function () { }); it('adds stroke classes for the tags of the outer way of multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - o = iD.Way({id: 'w-1', nodes: [a.id, b.id, c.id, a.id], tags: {natural: 'wood'}}), - i = iD.Way({id: 'w-2', nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: o.id, role: 'outer'}, {id: i.id, role: 'inner'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, o, i, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var o = iD.osmWay({id: 'w-1', nodes: [a.id, b.id, c.id, a.id], tags: {natural: 'wood'}}); + var i = iD.osmWay({id: 'w-2', nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: o.id, role: 'outer'}, {id: i.id, role: 'inner'}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, o, i, r]); surface.call(iD.svgLines(projection, context), graph, [i, o], all); @@ -84,14 +85,14 @@ describe('iD.svgLines', function () { }); describe('z-indexing', function() { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 1]}), - iD.Node({id: 'c', loc: [0, 0]}), - iD.Node({id: 'd', loc: [1, 1]}), - iD.Way({id: 'lo', tags: {highway: 'residential', tunnel: 'yes'}, nodes: ['a', 'b']}), - iD.Way({id: 'hi', tags: {highway: 'residential', bridge: 'yes'}, nodes: ['c', 'd']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 1]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [1, 1]}), + iD.osmWay({id: 'lo', tags: {highway: 'residential', tunnel: 'yes'}, nodes: ['a', 'b']}), + iD.osmWay({id: 'hi', tags: {highway: 'residential', bridge: 'yes'}, nodes: ['c', 'd']}) + ]); it('stacks higher lines above lower ones in a single render', function () { surface.call(iD.svgLines(projection, context), graph, [graph.entity('lo'), graph.entity('hi')], none); diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js index 92266c5c6..c30530fae 100644 --- a/test/spec/svg/midpoints.js +++ b/test/spec/svg/midpoints.js @@ -1,103 +1,100 @@ describe('iD.svgMidpoints', function () { - var context, surface, - selectedIDs = [], - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]), - filter = function() { return true; }; + var context, surface; + var _selectedIDs = []; + var filter = function() { return true; }; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(iD.geoZoomToScale(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { - context = iD.Context(); - context.mode = function() { - return { - id: 'select', - selectedIDs: function() { return selectedIDs; } - }; - }; + context = iD.coreContext(); + context.enter({ + id: 'select', + enter: function() { }, + exit: function() { }, + selectedIDs: function() { return _selectedIDs; } + }); + d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); + surface = context.surface(); }); it('creates midpoint on segment completely within the extent', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [50, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); - expect(surface.selectAll('.midpoint').datum().loc).to.eql([25, 0]); + expect(surface.selectAll('.midpoint').datum().loc).to.eql([0.5, 0]); }); it('doesn\'t create midpoint on segment with pixel length less than 40', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [39, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [0.0001, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); expect(surface.selectAll('.midpoint').nodes()).to.have.length(0); }); it('doesn\'t create midpoint on segment completely outside of the extent', function () { - var a = iD.Node({loc: [-100, 0]}), - b = iD.Node({loc: [-50, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); + var a = iD.osmNode({loc: [-1, 0]}); + var b = iD.osmNode({loc: [-0.5, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); expect(surface.selectAll('.midpoint').nodes()).to.have.length(0); }); it('creates midpoint on extent edge for segment partially outside of the extent', function () { - var a = iD.Node({loc: [50, 0]}), - b = iD.Node({loc: [500, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); + var a = iD.osmNode({loc: [0.5, 0]}); + var b = iD.osmNode({loc: [2, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); - expect(surface.selectAll('.midpoint').datum().loc).to.eql([100, 0]); + expect(surface.selectAll('.midpoint').datum().loc).to.eql([1, 0]); }); it('doesn\'t create midpoint on extent edge for segment with pixel length less than 20', function () { - var a = iD.Node({loc: [81, 0]}), - b = iD.Node({loc: [500, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); + var a = iD.osmNode({loc: [0.9999, 0]}); + var b = iD.osmNode({loc: [2, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); expect(surface.selectAll('.midpoint').nodes()).to.have.length(0); diff --git a/test/spec/svg/osm.js b/test/spec/svg/osm.js index e532d4580..43358b11f 100644 --- a/test/spec/svg/osm.js +++ b/test/spec/svg/osm.js @@ -7,13 +7,32 @@ describe('iD.svgOsm', function () { it('creates default osm layers', function () { container.call(iD.svgOsm()); - var nodes = container.selectAll('.layer-osm').nodes(); - expect(nodes.length).to.eql(5); - expect(d3.select(nodes[0]).classed('layer-areas')).to.be.true; - expect(d3.select(nodes[1]).classed('layer-lines')).to.be.true; - expect(d3.select(nodes[2]).classed('layer-hit')).to.be.true; - expect(d3.select(nodes[3]).classed('layer-halo')).to.be.true; - expect(d3.select(nodes[4]).classed('layer-label')).to.be.true; + var layers = container.selectAll('g.layer-osm').nodes(); + expect(layers.length).to.eql(4); + expect(d3.select(layers[0]).classed('layer-areas')).to.be.true; + expect(d3.select(layers[1]).classed('layer-lines')).to.be.true; + expect(d3.select(layers[2]).classed('layer-points')).to.be.true; + expect(d3.select(layers[3]).classed('layer-labels')).to.be.true; + }); + + it('creates default osm point layers', function () { + container.call(iD.svgOsm()); + var layers = container.selectAll('g.layer-points g.layer-points-group').nodes(); + expect(layers.length).to.eql(5); + expect(d3.select(layers[0]).classed('layer-points-points')).to.be.true; + expect(d3.select(layers[1]).classed('layer-points-midpoints')).to.be.true; + expect(d3.select(layers[2]).classed('layer-points-vertices')).to.be.true; + expect(d3.select(layers[3]).classed('layer-points-turns')).to.be.true; + expect(d3.select(layers[4]).classed('layer-points-targets')).to.be.true; + }); + + it('creates default osm label layers', function () { + container.call(iD.svgOsm()); + var layers = container.selectAll('g.layer-labels g.layer-labels-group').nodes(); + expect(layers.length).to.eql(3); + expect(d3.select(layers[0]).classed('layer-labels-halo')).to.be.true; + expect(d3.select(layers[1]).classed('layer-labels-label')).to.be.true; + expect(d3.select(layers[2]).classed('layer-labels-debug')).to.be.true; }); }); diff --git a/test/spec/svg/points.js b/test/spec/svg/points.js index dccca9d11..c1012a7cd 100644 --- a/test/spec/svg/points.js +++ b/test/spec/svg/points.js @@ -1,22 +1,22 @@ describe('iD.svgPoints', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]); + var context, surface; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(iD.geoZoomToScale(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); }); it('adds tag classes', function () { - var point = iD.Node({tags: {amenity: 'cafe'}, loc: [0, 0]}), - graph = iD.Graph([point]); + var point = iD.osmNode({tags: {amenity: 'cafe'}, loc: [0, 0]}); + var graph = iD.coreGraph([point]); surface.call(iD.svgPoints(projection, context), graph, [point]); diff --git a/test/spec/svg/tag_classes.js b/test/spec/svg/tag_classes.js index dffb8e14d..d454300d7 100644 --- a/test/spec/svg/tag_classes.js +++ b/test/spec/svg/tag_classes.js @@ -7,156 +7,156 @@ describe('iD.svgTagClasses', function () { it('adds no classes to elements whose datum has no tags', function() { selection - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(null); }); it('adds classes for primary tag key and key-value', function() { selection - .datum(iD.Entity({tags: {highway: 'primary'}})) + .datum(iD.osmEntity({tags: {highway: 'primary'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); it('adds only one primary tag', function() { selection - .datum(iD.Entity({tags: {highway: 'primary', railway: 'rail'}})) + .datum(iD.osmEntity({tags: {highway: 'primary', railway: 'rail'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); it('orders primary tags', function() { selection - .datum(iD.Entity({tags: {railway: 'rail', highway: 'primary'}})) + .datum(iD.osmEntity({tags: {railway: 'rail', highway: 'primary'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); it('adds status tag when status in primary value (`railway=abandoned`)', function() { selection - .datum(iD.Entity({tags: {railway: 'abandoned'}})) + .datum(iD.osmEntity({tags: {railway: 'abandoned'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-status tag-status-abandoned'); }); it('adds status tag when status in key and value matches "yes" (railway=rail + abandoned=yes)', function() { selection - .datum(iD.Entity({tags: {railway: 'rail', abandoned: 'yes'}})) + .datum(iD.osmEntity({tags: {railway: 'rail', abandoned: 'yes'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-railway-rail tag-status tag-status-abandoned'); }); it('adds status tag when status in key and value matches primary (railway=rail + abandoned=railway)', function() { selection - .datum(iD.Entity({tags: {railway: 'rail', abandoned: 'railway'}})) + .datum(iD.osmEntity({tags: {railway: 'rail', abandoned: 'railway'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-railway-rail tag-status tag-status-abandoned'); }); it('adds primary and status tag when status in key and no primary (abandoned=railway)', function() { selection - .datum(iD.Entity({tags: {abandoned: 'railway'}})) + .datum(iD.osmEntity({tags: {abandoned: 'railway'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-status tag-status-abandoned'); }); it('does not add status tag for different primary tag (highway=path + abandoned=railway)', function() { selection - .datum(iD.Entity({tags: {highway: 'path', abandoned: 'railway'}})) + .datum(iD.osmEntity({tags: {highway: 'path', abandoned: 'railway'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-path'); }); it('adds secondary tags', function() { selection - .datum(iD.Entity({tags: {highway: 'primary', bridge: 'yes'}})) + .datum(iD.osmEntity({tags: {highway: 'primary', bridge: 'yes'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary tag-bridge tag-bridge-yes'); }); it('adds no bridge=no tags', function() { selection - .datum(iD.Entity({tags: {bridge: 'no'}})) + .datum(iD.osmEntity({tags: {bridge: 'no'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(null); }); it('adds tag-unpaved for highway=track with no surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'track'}})) + .datum(iD.osmEntity({tags: {highway: 'track'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; }); it('does not add tag-unpaved for highway=track with explicit paved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'track', surface: 'asphalt'}})) + .datum(iD.osmEntity({tags: {highway: 'track', surface: 'asphalt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {highway: 'track', tracktype: 'grade1'}})) + .datum(iD.osmEntity({tags: {highway: 'track', tracktype: 'grade1'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); it('adds tag-unpaved for highway=track with explicit unpaved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'track', surface: 'dirt'}})) + .datum(iD.osmEntity({tags: {highway: 'track', surface: 'dirt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; selection - .datum(iD.Entity({tags: {highway: 'track', tracktype: 'grade3'}})) + .datum(iD.osmEntity({tags: {highway: 'track', tracktype: 'grade3'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; }); it('does not add tag-unpaved for other highway types with no surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'tertiary'}})) + .datum(iD.osmEntity({tags: {highway: 'tertiary'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {highway: 'foo'}})) + .datum(iD.osmEntity({tags: {highway: 'foo'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); it('does not add tag-unpaved for other highway types with explicit paved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'tertiary', surface: 'asphalt'}})) + .datum(iD.osmEntity({tags: {highway: 'tertiary', surface: 'asphalt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {highway: 'foo', tracktype: 'grade1'}})) + .datum(iD.osmEntity({tags: {highway: 'foo', tracktype: 'grade1'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); it('adds tag-unpaved for other highway types with explicit unpaved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'tertiary', surface: 'dirt'}})) + .datum(iD.osmEntity({tags: {highway: 'tertiary', surface: 'dirt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; selection - .datum(iD.Entity({tags: {highway: 'foo', tracktype: 'grade3'}})) + .datum(iD.osmEntity({tags: {highway: 'foo', tracktype: 'grade3'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; }); it('does not add tag-unpaved for non-highways', function() { selection - .datum(iD.Entity({tags: {railway: 'abandoned', surface: 'gravel'}})) + .datum(iD.osmEntity({tags: {railway: 'abandoned', surface: 'gravel'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {amenity: 'parking', surface: 'dirt'}})) + .datum(iD.osmEntity({tags: {amenity: 'parking', surface: 'dirt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); @@ -164,7 +164,7 @@ describe('iD.svgTagClasses', function () { it('adds tags based on the result of the `tags` accessor', function() { var primary = function () { return { highway: 'primary'}; }; selection - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses().tags(primary)); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); @@ -172,7 +172,7 @@ describe('iD.svgTagClasses', function () { it('removes classes for tags that are no longer present', function() { selection .attr('class', 'tag-highway tag-highway-primary') - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(''); }); @@ -180,7 +180,7 @@ describe('iD.svgTagClasses', function () { it('preserves existing non-"tag-"-prefixed classes', function() { selection .attr('class', 'selected') - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('selected'); }); @@ -188,7 +188,7 @@ describe('iD.svgTagClasses', function () { it('works on SVG elements', function() { selection = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'g')); selection - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(null); }); diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index f539db5fa..1c29ca70d 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -1,28 +1,30 @@ describe('iD.svgVertices', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]); + var context; + var surface; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(iD.geoZoomToScale(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); + beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); }); it('adds the .shared class to vertices that are members of two or more ways', function () { - var zoom = 17, - node = iD.Node({loc: [0, 0]}), - way1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - way2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - graph = iD.Graph([node, way1, way2]); - - surface.call(iD.svgVertices(projection, context), graph, [node], zoom); + var node = iD.osmNode({loc: [0, 0]}); + var way1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}); + var way2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}); + var graph = iD.coreGraph([node, way1, way2]); + var filter = function() { return true; }; + var extent = iD.geoExtent([0, 0], [1, 1]); + surface.call(iD.svgVertices(projection, context), graph, [node], filter, extent); expect(surface.select('.vertex').classed('shared')).to.be.true; }); });