diff --git a/css/50_misc.css b/css/50_misc.css index 531b09a0d..be90c5e73 100644 --- a/css/50_misc.css +++ b/css/50_misc.css @@ -71,7 +71,7 @@ path.stroke.tag-barrier { /* bridges */ path.casing.tag-bridge { stroke-opacity: 0.6; - stroke: #000; + stroke: #000 !important; stroke-linecap: butt; stroke-dasharray: none; } diff --git a/css/60_photos.css b/css/60_photos.css index fd4109720..cc853900c 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -19,16 +19,52 @@ border-radius: 0; padding: 5px; position: absolute; - right: 0; - top: 0; - z-index: 48; + right: 5px; + top: 5px; + z-index: 50; } +#photoviewer button.resize-handle-xy { + border-radius: 0; + position: absolute; + top: 0; + right: 0; + z-index: 49; + cursor: nesw-resize; + height: 25px; + width: 25px; +} + +#photoviewer button.resize-handle-x { + border-radius: 0; + position: absolute; + top: 0; + right: 0; + bottom: 0; + z-index: 48; + cursor: ew-resize; + height: auto; + width: 6px; +} + +#photoviewer button.resize-handle-y { + border-radius: 0; + position: absolute; + top: 0; + right: 0; + z-index: 48; + cursor: ns-resize; + height: 6px; + width: 100%; +} + + .photo-wrapper, .photo-wrapper img { width: 100%; height: 100%; overflow: hidden; + object-fit: cover; } .photo-wrapper .photo-attribution { @@ -186,7 +222,7 @@ flex-flow: row nowrap; justify-content: space-between; align-items: center; - pading: 0 5px; + padding: 0 5px; } .ms-wrapper .photo-attribution .image-view-link { text-align: left; @@ -262,6 +298,8 @@ label.streetside-hires { } .osc-image-wrap { + width: 100%; + height: 100%; transform-origin:0 0; -ms-transform-origin:0 0; -webkit-transform-origin:0 0; diff --git a/css/65_data.css b/css/65_data.css index fbad3ba9e..07146df57 100644 --- a/css/65_data.css +++ b/css/65_data.css @@ -127,12 +127,16 @@ border-radius: 20px; } .comment-main { - padding: 10px; + padding: 10px 10px 10px 0; flex: 1 1 100%; flex-flow: column nowrap; overflow: hidden; overflow-wrap: break-word; } +[dir='rtl'] .comment-main { + padding: 10px 0 10px 10px; +} + .comment-metadata { flex-flow: row nowrap; justify-content: space-between; @@ -154,14 +158,18 @@ border-left: none; } -#new-comment-input { +.note-save { + padding: 10px; +} + +.note-save #new-comment-input { width: 100%; height: 100px; max-height: 300px; min-height: 100px; } -.note-save-section { +.note-save .detail-section { margin: 10px 0; } diff --git a/css/80_app.css b/css/80_app.css index 7475ffd59..c6c8c0d91 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -607,8 +607,13 @@ button.save.has-count .count::before { margin-right: 5px; } [dir='rtl'] .icon.pre-text { - margin-left: 5px; - margin-right: 0; + margin-left: 5px; + margin-right: 0; +} + +.icon.pre-text.user-icon { + margin-left: 5px; + margin-right: 5px; } .icon.light { @@ -1320,6 +1325,12 @@ a.hide-toggle { border: 1px solid #ccc; } +/* no scrollbars */ +.inspector-hover div { + overflow-x: hidden; + overflow-y: hidden; +} + /* hide and remove from layout */ .inspector-hidden, .inspector-hover label input[type="checkbox"], @@ -3841,7 +3852,6 @@ svg.mouseclick use.right { } - /* Save Mode ------------------------------------------------------- */ .mode-save a.user-info { @@ -3870,6 +3880,7 @@ svg.mouseclick use.right { color: #fff; } +.note-save .field-warning, .mode-save .field-warning { background: #ffb; border: 1px solid #ccc; @@ -3877,6 +3888,7 @@ svg.mouseclick use.right { padding: 10px; } +.note-save .field-warning:empty, .mode-save .field-warning:empty { display: none; } diff --git a/data/core.yaml b/data/core.yaml index 9827276fd..125a58538 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -280,8 +280,8 @@ en: localized_translation_language: Choose language localized_translation_name: Name zoom_in_edit: Zoom in to edit - login: login - logout: logout + login: Log In + logout: Log Out loading_auth: "Connecting to OpenStreetMap..." report_a_bug: Report a bug help_translate: Help translate @@ -638,6 +638,9 @@ en: new: New Note newDescription: "Describe the issue." newNote: Add Note + login: You must log in to change or comment on this note. + upload_explanation: "Your comments will be publicly visible to all OpenStreetMap users." + upload_explanation_with_user: "Your comments as {user} will be publicly visible to all OpenStreetMap users." help: title: Help key: H diff --git a/dist/locales/en.json b/dist/locales/en.json index 0ecfd4606..a7599f7fb 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -358,8 +358,8 @@ "localized_translation_name": "Name" }, "zoom_in_edit": "Zoom in to edit", - "login": "login", - "logout": "logout", + "login": "Log In", + "logout": "Log Out", "loading_auth": "Connecting to OpenStreetMap...", "report_a_bug": "Report a bug", "help_translate": "Help translate", @@ -772,7 +772,10 @@ "report": "Report", "new": "New Note", "newDescription": "Describe the issue.", - "newNote": "Add Note" + "newNote": "Add Note", + "login": "You must log in to change or comment on this note.", + "upload_explanation": "Your comments will be publicly visible to all OpenStreetMap users.", + "upload_explanation_with_user": "Your comments as {user} will be publicly visible to all OpenStreetMap users." }, "help": { "title": "Help", diff --git a/modules/lib/d3.geo.tile.js b/modules/lib/d3.geo.tile.js deleted file mode 100644 index 3b0f718ef..000000000 --- a/modules/lib/d3.geo.tile.js +++ /dev/null @@ -1,93 +0,0 @@ -import { range as d3_range } from 'd3-array'; - - -export function d3geoTile() { - var _size = [960, 500]; - var _scale = 256; - var _scaleExtent = [0, 20]; - var _translate = [_size[0] / 2, _size[1] / 2]; - var _zoomDelta = 0; - var _margin = 0; - - function bound(val) { - return Math.min(_scaleExtent[1], Math.max(_scaleExtent[0], val)); - } - - function tile() { - var z = Math.max(Math.log(_scale) / Math.LN2 - 8, 0); - var z0 = bound(Math.round(z + _zoomDelta)); - var k = Math.pow(2, z - z0 + 8); - var origin = [ - (_translate[0] - _scale / 2) / k, - (_translate[1] - _scale / 2) / k - ]; - - var cols = d3_range( - Math.max(0, Math.floor(-origin[0]) - _margin), - Math.max(0, Math.ceil(_size[0] / k - origin[0]) + _margin) - ); - var rows = d3_range( - Math.max(0, Math.floor(-origin[1]) - _margin), - Math.max(0, Math.ceil(_size[1] / k - origin[1]) + _margin) - ); - - var tiles = []; - for (var i = 0; i < rows.length; i++) { - var y = rows[i]; - for (var j = 0; j < cols.length; j++) { - var x = cols[j]; - - if (i >= _margin && i <= rows.length - _margin && - j >= _margin && j <= cols.length - _margin) { - tiles.unshift([x, y, z0]); // tiles in view at beginning - } else { - tiles.push([x, y, z0]); // tiles in margin at the end - } - } - } - - tiles.translate = origin; - tiles.scale = k; - - return tiles; - } - - tile.scaleExtent = function(val) { - if (!arguments.length) return _scaleExtent; - _scaleExtent = val; - return tile; - }; - - tile.size = function(val) { - if (!arguments.length) return _size; - _size = val; - return tile; - }; - - tile.scale = function(val) { - if (!arguments.length) return _scale; - _scale = val; - return tile; - }; - - tile.translate = function(val) { - if (!arguments.length) return _translate; - _translate = val; - return tile; - }; - - tile.zoomDelta = function(val) { - if (!arguments.length) return _zoomDelta; - _zoomDelta = +val; - return tile; - }; - - // number to extend the rows/columns beyond those covering the viewport - tile.margin = function(val) { - if (!arguments.length) return _margin; - _margin = +val; - return tile; - }; - - return tile; -} diff --git a/modules/lib/index.js b/modules/lib/index.js index b6ddad54a..bee98416c 100644 --- a/modules/lib/index.js +++ b/modules/lib/index.js @@ -1,3 +1,2 @@ export { d3combobox } from './d3.combobox'; -export { d3geoTile } from './d3.geo.tile'; export { d3keybinding } from './d3.keybinding'; diff --git a/modules/renderer/tile_layer.js b/modules/renderer/tile_layer.js index 475147e43..d5c59e9b9 100644 --- a/modules/renderer/tile_layer.js +++ b/modules/renderer/tile_layer.js @@ -1,15 +1,14 @@ import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; -import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoScaleToZoom, geoVecLength } from '../geo'; -import { utilPrefixCSSProperty } from '../util'; +import { utilPrefixCSSProperty, utilTile } from '../util'; export function rendererTileLayer(context) { var tileSize = 256; var transformProp = utilPrefixCSSProperty('Transform'); - var geotile = d3_geoTile(); + var geotile = utilTile(); var _projection; var _cache = {}; diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index 80340d790..cdf04aa3b 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -18,10 +18,12 @@ import { import rbush from 'rbush'; -import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoExtent } from '../geo'; import { svgDefs } from '../svg'; -import { utilQsString, utilRebind } from '../util'; +import { utilDetect } from '../util/detect'; +import { utilQsString, utilRebind, utilTile } from '../util'; + +var geoTile = utilTile(); var apibase = 'https://a.mapillary.com/v3/'; var viewercss = 'mapillary-js/mapillary.min.css'; @@ -63,33 +65,18 @@ function maxPageAtZoom(z) { if (z > 18) return 80; } -function getTiles(projection) { - 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] - ]; - return d3_geoTile() - .scaleExtent([tileZoom, tileZoom]) - .scale(s) - .size(projection.clipExtent()[1]) - .translate(projection.translate())() - .map(function(tile) { - var x = tile[0] * ts - origin[0]; - var y = tile[1] * ts - origin[1]; - - return { - id: tile.toString(), - xyz: tile, - extent: geoExtent( - projection.invert([x, y + ts]), - projection.invert([x + ts, y]) - ) - }; - }); +function localeTimestamp(s) { + if (!s) return null; + var detected = utilDetect(); + var options = { + day: 'numeric', month: 'short', year: 'numeric', + hour: 'numeric', minute: 'numeric', second: 'numeric', + timeZone: 'UTC' + }; + var d = new Date(s); + if (isNaN(d.getTime())) return null; + return d.toLocaleString(detected.locale, options); } @@ -97,15 +84,11 @@ function loadTiles(which, url, projection) { var s = projection.scale() * 2 * Math.PI; var currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0)); - var tiles = getTiles(projection).filter(function(t) { - return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]); - }); + var dimension = projection.clipExtent()[1]; + var tiles = geoTile.getTiles(projection, dimension, tileZoom, 0); + tiles = geoTile.filterNullIsland(tiles); - _filter(which.inflight, function(v, k) { - var wanted = _find(tiles, function(tile) { return k === (tile.id + ',0'); }); - if (!wanted) delete which.inflight[k]; - return !wanted; - }).map(abortRequest); + geoTile.removeInflightRequests(which, tiles, abortRequest, ',0'); tiles.forEach(function(tile) { loadNextTilePage(which, currZoom, url, tile); @@ -410,6 +393,13 @@ export default { // load mapillary signs sprite var defs = context.container().select('defs'); defs.call(svgDefs(context).addSprites, ['mapillary-sprite']); + + // Register viewer resize handler + context.ui().on('photoviewerResize', function() { + if (_mlyViewer) { + _mlyViewer.resize(); + } + }); }, diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index 14ded2d6b..73483dddc 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -22,9 +22,9 @@ import { import rbush from 'rbush'; -import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoExtent } from '../geo'; +import { utilTile } from '../util'; import { utilDetect } from '../util/detect'; import { @@ -33,18 +33,19 @@ import { utilSetTransform } from '../util'; +var geoTile = utilTile(); -var apibase = 'https://openstreetcam.org', - maxResults = 1000, - tileZoom = 14, - dispatch = d3_dispatch('loadedImages'), - imgZoom = d3_zoom() - .extent([[0, 0], [320, 240]]) - .translateExtent([[0, 0], [320, 240]]) - .scaleExtent([1, 15]) - .on('zoom', zoomPan), - _oscCache, - _oscSelectedImage; +var apibase = 'https://openstreetcam.org'; +var maxResults = 1000; +var tileZoom = 14; +var dispatch = d3_dispatch('loadedImages'); +var imgZoom = d3_zoom() + .extent([[0, 0], [320, 240]]) + .translateExtent([[0, 0], [320, 240]]) + .scaleExtent([1, 15]) + .on('zoom', zoomPan); +var _oscCache; +var _oscSelectedImage; function abortRequest(i) { @@ -74,48 +75,15 @@ function maxPageAtZoom(z) { } -function getTiles(projection) { - var 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]]; - - return d3_geoTile() - .scaleExtent([tileZoom, tileZoom]) - .scale(s) - .size(projection.clipExtent()[1]) - .translate(projection.translate())() - .map(function(tile) { - var x = tile[0] * ts - origin[0], - y = tile[1] * ts - origin[1]; - - return { - id: tile.toString(), - xyz: tile, - extent: geoExtent( - projection.invert([x, y + ts]), - projection.invert([x + ts, y]) - ) - }; - }); -} - - function loadTiles(which, url, projection) { var s = projection.scale() * 2 * Math.PI, currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0)); - var tiles = getTiles(projection).filter(function(t) { - return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]); - }); + var dimension = projection.clipExtent()[1]; + var tiles = geoTile.getTiles(projection, dimension, tileZoom, 0); + tiles = geoTile.filterNullIsland(tiles); - _filter(which.inflight, function(v, k) { - var wanted = _find(tiles, function(tile) { return k === (tile.id + ',0'); }); - if (!wanted) delete which.inflight[k]; - return !wanted; - }).map(abortRequest); + geoTile.removeInflightRequests(which, tiles, abortRequest, ',0'); tiles.forEach(function(tile) { loadNextTilePage(which, currZoom, url, tile); @@ -129,12 +97,12 @@ function loadNextTilePage(which, currZoom, url, tile) { var maxPages = maxPageAtZoom(currZoom); var nextPage = cache.nextPage[tile.id] || 1; var params = utilQsString({ - ipp: maxResults, - page: nextPage, - // client_id: clientId, - bbTopLeft: [bbox.maxY, bbox.minX].join(','), - bbBottomRight: [bbox.minY, bbox.maxX].join(',') - }, true); + ipp: maxResults, + page: nextPage, + // client_id: clientId, + bbTopLeft: [bbox.maxY, bbox.minX].join(','), + bbBottomRight: [bbox.minY, bbox.maxX].join(',') + }, true); if (nextPage > maxPages) return; @@ -367,6 +335,16 @@ export default { .attr('class', 'osc-image-wrap'); + // Register viewer resize handler + context.ui().on('photoviewerResize', function(dimensions) { + imgZoom = d3_zoom() + .extent([[0, 0], dimensions]) + .translateExtent([[0, 0], dimensions]) + .scaleExtent([1, 15]) + .on('zoom', zoomPan); + }); + + function rotate(deg) { return function() { if (!_oscSelectedImage) return; diff --git a/modules/services/osm.js b/modules/services/osm.js index 7b641715f..079a64c1a 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -17,7 +17,6 @@ import { xml as d3_xml } from 'd3-request'; import osmAuth from 'osm-auth'; import { JXON } from '../util/jxon'; -import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoExtent, geoVecAdd } from '../geo'; import { @@ -31,9 +30,11 @@ import { import { utilRebind, utilIdleWorker, + utilTile, utilQsString } from '../util'; +var geoTile = utilTile(); var dispatch = d3_dispatch('authLoading', 'authDone', 'change', 'loading', 'loaded', 'loadedNotes'); var urlroot = 'https://www.openstreetmap.org'; @@ -789,44 +790,13 @@ export default { tilezoom = _tileZoom; } - 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] - ]; - - // what tiles cover the view? - var tiler = d3_geoTile() - .scaleExtent([tilezoom, tilezoom]) - .scale(s) - .size(dimensions) - .translate(projection.translate()); - - var tiles = tiler().map(function(tile) { - var x = tile[0] * ts - origin[0]; - var y = tile[1] * ts - origin[1]; - - return { - id: tile.toString(), - extent: geoExtent( - projection.invert([x, y + ts]), - projection.invert([x + ts, y]) - ) - }; - }); + // get tiles + var tiles = geoTile.getTiles(projection, dimensions, tilezoom, 0); + tiles = geoTile.filterNullIsland(tiles); // remove inflight requests that no longer cover the view.. var hadRequests = !_isEmpty(cache.inflight); - _filter(cache.inflight, function(v, i) { - var wanted = _find(tiles, function(tile) { return i === tile.id; }); - if (!wanted) { - delete cache.inflight[i]; - } - return !wanted; - }).map(abortRequest); - + geoTile.removeInflightRequests(cache, tiles, abortRequest); if (hadRequests && !loadingNotes && _isEmpty(cache.inflight)) { dispatch.call('loaded'); // stop the spinner } diff --git a/modules/services/streetside.js b/modules/services/streetside.js index f13470b6c..52ee2c14f 100644 --- a/modules/services/streetside.js +++ b/modules/services/streetside.js @@ -17,7 +17,6 @@ import { import rbush from 'rbush'; import { t } from '../util/locale'; import { jsonpRequest } from '../util/jsonp_request'; -import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoExtent, @@ -29,10 +28,12 @@ import { } from '../geo'; import { utilDetect } from '../util/detect'; -import { utilQsString, utilRebind } from '../util'; +import { utilQsString, utilRebind, utilTile } from '../util'; import Q from 'q'; +var geoTile = utilTile(); + var bubbleApi = 'https://dev.virtualearth.net/mapcontrol/HumanScaleServices/GetBubbles.ashx?'; var streetsideImagesApi = 'https://t.ssl.ak.tiles.virtualearth.net/tiles/'; var bubbleAppKey = 'AuftgJsO0Xs8Ts4M1xZUQJQXJNsvmh3IV8DkNieCiy3tCwCUMq76-WpkrBtNAuEm'; @@ -85,46 +86,6 @@ function localeTimestamp(s) { return d.toLocaleString(detected.locale, options); } -/** - * getTiles() returns array of d3 geo tiles. - * Using d3.geo.tiles.js from lib, gets tile extents for each grid tile in a grid created from - * an area around (and including) the current map view extents. - */ -function getTiles(projection, margin) { - // s is the current map scale - // z is the 'Level of Detail', or zoom-level, where Level 1 is far from the earth, and Level 23 is close to the ground. - // ts ('tile size') here is the formula for determining the width/height of the map in pixels, but with a modification. - // See 'Ground Resolution and Map Scale': //https://msdn.microsoft.com/en-us/library/bb259689.aspx. - // As used here, by subtracting constant 'tileZoom' from z (the level), you end up with a much smaller value for the tile size (in pixels). - 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 tiler = d3_geoTile() - .scaleExtent([tileZoom, tileZoom]) - .scale(s) - .size(projection.clipExtent()[1]) - .translate(projection.translate()) - .margin(margin || 0); // request nearby tiles so we can connect sequences. - - return tiler() - .map(function(tile) { - var x = tile[0] * ts - origin[0]; - var y = tile[1] * ts - origin[1]; - return { - id: tile.toString(), - xyz: tile, - extent: geoExtent( - projection.invert([x, y + ts]), - projection.invert([x + ts, y]) - ) - }; - }); -} /** * loadTiles() wraps the process of generating tiles and then fetching image points for each tile. @@ -133,10 +94,9 @@ function loadTiles(which, url, projection, margin) { var s = projection.scale() * 2 * Math.PI; var currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0)); - // breakup the map view into tiles - var tiles = getTiles(projection, margin).filter(function (t) { - return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]); - }); + var dimension = projection.clipExtent()[1]; + var tiles = geoTile.getTiles(projection, dimension, tileZoom, margin); + tiles = geoTile.filterNullIsland(tiles); tiles.forEach(function (tile) { loadNextTilePage(which, currZoom, url, tile); @@ -669,6 +629,14 @@ export default { .attr('src', context.asset(pannellumViewerJS)); + // Register viewer resize handler + context.ui().on('photoviewerResize', function() { + if (_pannellumViewer) { + _pannellumViewer.resize(); + } + }); + + function step(stepBy) { return function() { var viewer = d3_select('#photoviewer'); diff --git a/modules/ui/commit.js b/modules/ui/commit.js index ec79e7baf..8cf633cdf 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -167,7 +167,7 @@ export function uiCommit(context) { userLink .append('a') - .attr('class','user-info') + .attr('class', 'user-info') .text(user.display_name) .attr('href', osm.userURL(user.display_name)) .attr('tabindex', -1) diff --git a/modules/ui/init.js b/modules/ui/init.js index 7e1d134fc..4059f9420 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -2,6 +2,7 @@ import { event as d3_event, select as d3_select } from 'd3-selection'; +import { dispatch as d3_dispatch } from 'd3-dispatch'; import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; @@ -13,6 +14,7 @@ import { modeBrowse } from '../modes'; import { services } from '../services'; import { svgDefs, svgIcon } from '../svg'; import { utilGetDimensions } from '../util/dimensions'; +import { utilRebind } from '../util'; import { uiAccount } from './account'; import { uiAttribution } from './attribution'; @@ -45,6 +47,7 @@ import { uiCmd } from './cmd'; export function uiInit(context) { var uiInitCounter = 0; + var dispatch = d3_dispatch('photoviewerResize'); function render(container) { @@ -256,7 +259,33 @@ export function uiInit(context) { .append('div') .call(svgIcon('#iD-icon-close')); + photoviewer + .append('button') + .attr('class', 'resize-handle-xy') + .on( + 'mousedown', + buildResizeListener(photoviewer, 'photoviewerResize', dispatch, { resizeOnX: true, resizeOnY: true }) + ); + photoviewer + .append('button') + .attr('class', 'resize-handle-x') + .on( + 'mousedown', + buildResizeListener(photoviewer, 'photoviewerResize', dispatch, { resizeOnX: true }) + ); + + photoviewer + .append('button') + .attr('class', 'resize-handle-y') + .on( + 'mousedown', + buildResizeListener(photoviewer, 'photoviewerResize', dispatch, { resizeOnY: true }) + ); + + var mapDimensions = map.dimensions(); + + // bind events window.onbeforeunload = function() { return context.save(); }; @@ -265,30 +294,13 @@ export function uiInit(context) { context.history().unlock(); }; - var mapDimensions = map.dimensions(); - - - function onResize() { - mapDimensions = utilGetDimensions(content, true); - map.dimensions(mapDimensions); - } - d3_select(window) .on('resize.editor', onResize); onResize(); - function pan(d) { - return function() { - d3_event.preventDefault(); - context.pan(d, 100); - }; - } - - - // pan amount - var pa = 80; + var pa = 80; // pan amount var keybinding = d3_keybinding('main') .on('⌫', function() { d3_event.preventDefault(); }) .on('←', pan([pa, 0])) @@ -316,8 +328,8 @@ export function uiInit(context) { .call(uiShortcuts(context)); } - var osm = context.connection(), - auth = uiLoading(context).message(t('loading_auth')).blocking(true); + var osm = context.connection(); + var auth = uiLoading(context).message(t('loading_auth')).blocking(true); if (osm && auth) { osm @@ -336,6 +348,85 @@ export function uiInit(context) { hash.startWalkthrough = false; context.container().call(uiIntro(context)); } + + + function onResize() { + mapDimensions = utilGetDimensions(content, true); + map.dimensions(mapDimensions); + + // shrink photo viewer if it is too big + // (-90 preserves space at top and bottom of map used by menus) + var photoDimensions = utilGetDimensions(photoviewer, true); + if (photoDimensions[0] > mapDimensions[0] || photoDimensions[1] > (mapDimensions[1] - 90)) { + var setPhotoDimensions = [ + Math.min(photoDimensions[0], mapDimensions[0]), + Math.min(photoDimensions[1], mapDimensions[1] - 90), + ]; + + photoviewer + .style('width', setPhotoDimensions[0] + 'px') + .style('height', setPhotoDimensions[1] + 'px'); + + dispatch.call('photoviewerResize', photoviewer, setPhotoDimensions); + } + } + + + function pan(d) { + return function() { + d3_event.preventDefault(); + context.pan(d, 100); + }; + } + + function buildResizeListener(target, eventName, dispatch, options) { + var resizeOnX = !!options.resizeOnX; + var resizeOnY = !!options.resizeOnY; + var minHeight = options.minHeight || 240; + var minWidth = options.minWidth || 320; + var startX; + var startY; + var startWidth; + var startHeight; + + function startResize() { + var mapSize = context.map().dimensions(); + + if (resizeOnX) { + var maxWidth = mapSize[0]; + var newWidth = clamp((startWidth + d3_event.clientX - startX), minWidth, maxWidth); + target.style('width', newWidth + 'px'); + } + + if (resizeOnY) { + var maxHeight = mapSize[1] - 90; // preserve space at top/bottom of map + var newHeight = clamp((startHeight + startY - d3_event.clientY), minHeight, maxHeight); + target.style('height', newHeight + 'px'); + } + + dispatch.call(eventName, target, utilGetDimensions(target, true)); + } + + function clamp(num, min, max) { + return Math.max(min, Math.min(num, max)); + } + + function stopResize() { + d3_select(window) + .on('.' + eventName, null); + } + + return function initResize() { + startX = d3_event.clientX; + startY = d3_event.clientY; + startWidth = target.node().getBoundingClientRect().width; + startHeight = target.node().getBoundingClientRect().height; + + d3_select(window) + .on('mousemove.' + eventName, startResize, false) + .on('mouseup.' + eventName, stopResize, false); + }; + } } @@ -370,5 +461,5 @@ export function uiInit(context) { ui.sidebar = uiSidebar(context); - return ui; + return utilRebind(ui, dispatch, 'on'); } diff --git a/modules/ui/note_comments.js b/modules/ui/note_comments.js index 7d35b9935..ccf3d5537 100644 --- a/modules/ui/note_comments.js +++ b/modules/ui/note_comments.js @@ -66,7 +66,7 @@ export function uiNoteComments() { mainEnter .append('div') .attr('class', 'comment-text') - .text(function(d) { return d.text; }); + .html(function(d) { return d.html; }); comments .call(replaceAvatars); @@ -101,6 +101,7 @@ export function uiNoteComments() { if (!s) return null; var detected = utilDetect(); var options = { day: 'numeric', month: 'short', year: 'numeric' }; + s = s.replace(/-/g, '/'); // fix browser-specific Date() issues var d = new Date(s); if (isNaN(d.getTime())) return null; return d.toLocaleDateString(detected.locale, options); diff --git a/modules/ui/note_editor.js b/modules/ui/note_editor.js index 865b298e4..1ded94d46 100644 --- a/modules/ui/note_editor.js +++ b/modules/ui/note_editor.js @@ -1,5 +1,8 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; -import { select as d3_select } from 'd3-selection'; +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; import { t } from '../util/locale'; import { services } from '../services'; @@ -53,29 +56,42 @@ export function uiNoteEditor(context) { .attr('class', 'body') .merge(body); - body.selectAll('.note-editor') - .data([0]) - .enter() + var editor = body.selectAll('.note-editor') + .data([0]); + + editor = editor.enter() .append('div') .attr('class', 'modal-section note-editor') + .merge(editor) .call(noteHeader.note(_note)) .call(noteComments.note(_note)) - .call(noteSave); + .call(noteSaveSection); - selection.selectAll('.footer') - .data([0]) - .enter() + var footer = selection.selectAll('.footer') + .data([0]); + + footer = footer.enter() .append('div') .attr('class', 'footer') + .merge(footer) .call(uiViewOnOSM(context).what(_note)) .call(uiNoteReport(context).note(_note)); + + + // rerender the note editor on any auth change + var osm = services.osm; + if (osm) { + osm.on('change.note-save', function() { + selection.call(noteEditor); + }); + } } - function noteSave(selection) { + function noteSaveSection(selection) { var isSelected = (_note && _note.id === context.selectedNoteID()); - var noteSave = selection.selectAll('.note-save-section') + var noteSave = selection.selectAll('.note-save') .data((isSelected ? [_note] : []), function(d) { return d.status + d.id; }); // exit @@ -85,7 +101,7 @@ export function uiNoteEditor(context) { // enter var noteSaveEnter = noteSave.enter() .append('div') - .attr('class', 'note-save-section save-section cf'); + .attr('class', 'note-save save-section cf'); noteSaveEnter .append('h4') @@ -107,6 +123,7 @@ export function uiNoteEditor(context) { // update noteSave = noteSaveEnter .merge(noteSave) + .call(userDetails) .call(noteSaveButtons); @@ -128,7 +145,100 @@ export function uiNoteEditor(context) { } + function userDetails(selection) { + var detailSection = selection.selectAll('.detail-section') + .data([0]); + + detailSection = detailSection.enter() + .append('div') + .attr('class', 'detail-section') + .merge(detailSection); + + var osm = services.osm; + if (!osm) return; + + // Add warning if user is not logged in + var hasAuth = osm.authenticated(); + var authWarning = detailSection.selectAll('.auth-warning') + .data(hasAuth ? [] : [0]); + + authWarning.exit() + .transition() + .duration(200) + .style('opacity', 0) + .remove(); + + var authEnter = authWarning.enter() + .insert('div', '.tag-reference-body') + .attr('class', 'field-warning auth-warning') + .style('opacity', 0); + + authEnter + .call(svgIcon('#iD-icon-alert', 'inline')); + + authEnter + .append('span') + .text(t('note.login')); + + authEnter + .append('a') + .attr('target', '_blank') + .call(svgIcon('#iD-icon-out-link', 'inline')) + .append('span') + .text(t('login')) + .on('click.note-login', function() { + d3_event.preventDefault(); + osm.authenticate(); + }); + + authEnter + .transition() + .duration(200) + .style('opacity', 1); + + + var prose = detailSection.selectAll('.note-save-prose') + .data(hasAuth ? [0] : []); + + prose.exit() + .remove(); + + prose = prose.enter() + .append('p') + .attr('class', 'note-save-prose') + .text(t('note.upload_explanation')) + .merge(prose); + + osm.userDetails(function(err, user) { + if (err) return; + + var userLink = d3_select(document.createElement('div')); + + if (user.image_url) { + userLink + .append('img') + .attr('src', user.image_url) + .attr('class', 'icon pre-text user-icon'); + } + + userLink + .append('a') + .attr('class', 'user-info') + .text(user.display_name) + .attr('href', osm.userURL(user.display_name)) + .attr('tabindex', -1) + .attr('target', '_blank'); + + prose + .html(t('note.upload_explanation_with_user', { user: userLink.html() })); + }); + } + + function noteSaveButtons(selection) { + var osm = services.osm; + var hasAuth = osm && osm.authenticated(); + var isSelected = (_note && _note.id === context.selectedNoteID()); var buttonSection = selection.selectAll('.buttons') .data((isSelected ? [_note] : []), function(d) { return d.status + d.id; }); @@ -168,6 +278,7 @@ export function uiNoteEditor(context) { .merge(buttonEnter); buttonSection.select('.status-button') // select and propagate data + .attr('disabled', (hasAuth ? null : true)) .text(function(d) { var action = (d.status === 'open' ? 'close' : 'open'); var andComment = (d.newComment ? '_comment' : ''); @@ -186,7 +297,7 @@ export function uiNoteEditor(context) { buttonSection.select('.comment-button') // select and propagate data .attr('disabled', function(d) { - return (d.status === 'open' && d.newComment) ? null : true; + return (hasAuth && d.status === 'open' && d.newComment) ? null : true; }) .on('click.save', function(d) { this.blur(); // avoid keeping focus on the button - #4641 diff --git a/modules/util/index.js b/modules/util/index.js index f64856de8..9673bfef5 100644 --- a/modules/util/index.js +++ b/modules/util/index.js @@ -23,5 +23,6 @@ export { utilSessionMutex } from './session_mutex'; export { utilStringQs } from './util'; export { utilSuggestNames } from './suggest_names'; export { utilTagText } from './util'; +export { utilTile } from './tile'; export { utilTriggerEvent } from './trigger_event'; export { utilWrap } from './util'; diff --git a/modules/util/tile.js b/modules/util/tile.js new file mode 100644 index 000000000..2179525f7 --- /dev/null +++ b/modules/util/tile.js @@ -0,0 +1,179 @@ +import _filter from 'lodash-es/filter'; +import _find from 'lodash-es/find'; +import { range as d3_range } from 'd3-array'; +import { geoExtent } from '../geo'; + + +export function utilTile() { + var _size = [960, 500]; + var _scale = 256; + var _scaleExtent = [0, 20]; + var _translate = [_size[0] / 2, _size[1] / 2]; + var _zoomDelta = 0; + var _margin = 0; + + function bound(val) { + return Math.min(_scaleExtent[1], Math.max(_scaleExtent[0], val)); + } + + function nearNullIsland(x, y, z) { + if (z >= 7) { + 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; + } + + function tile() { + var z = Math.max(Math.log(_scale) / Math.LN2 - 8, 0); + var z0 = bound(Math.round(z + _zoomDelta)); + var k = Math.pow(2, z - z0 + 8); + var origin = [ + (_translate[0] - _scale / 2) / k, + (_translate[1] - _scale / 2) / k + ]; + + var cols = d3_range( + Math.max(0, Math.floor(-origin[0]) - _margin), + Math.max(0, Math.ceil(_size[0] / k - origin[0]) + _margin) + ); + var rows = d3_range( + Math.max(0, Math.floor(-origin[1]) - _margin), + Math.max(0, Math.ceil(_size[1] / k - origin[1]) + _margin) + ); + + var tiles = []; + for (var i = 0; i < rows.length; i++) { + var y = rows[i]; + for (var j = 0; j < cols.length; j++) { + var x = cols[j]; + + if (i >= _margin && i <= rows.length - _margin && + j >= _margin && j <= cols.length - _margin) { + tiles.unshift([x, y, z0]); // tiles in view at beginning + } else { + tiles.push([x, y, z0]); // tiles in margin at the end + } + } + } + + tiles.translate = origin; + tiles.scale = k; + + return tiles; + } + + + /** + * getTiles() returns array of d3 geo tiles. + * Using d3.geo.tiles.js from lib, gets tile extents for each grid tile in a grid created from + * an area around (and including) the current map view extents. + */ + tile.getTiles = function(projection, dimensions, tilezoom, margin) { + + // s is the current map scale + // z is the 'Level of Detail', or zoom-level, where Level 1 is far from the earth, and Level 23 is close to the ground. + // ts ('tile size') here is the formula for determining the width/height of the map in pixels, but with a modification. + // See 'Ground Resolution and Map Scale': //https://msdn.microsoft.com/en-us/library/bb259689.aspx. + // As used here, by subtracting constant 'tileZoom' from z (the level), you end up with a much smaller value for the tile size (in pixels). + 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 tiler = this + .scaleExtent([tilezoom, tilezoom]) + .scale(s) + .size(dimensions) + .translate(projection.translate()) + .margin(margin || 0); // request nearby tiles so we can connect sequences. + + var tiles = tiler() + .map(function(tile) { + var x = tile[0] * ts - origin[0]; + var y = tile[1] * ts - origin[1]; + + return { + id: tile.toString(), + xyz: tile, + extent: geoExtent( + projection.invert([x, y + ts]), + projection.invert([x + ts, y]) + ) + }; + }); + + return tiles; + }; + + + tile.filterNullIsland = function(tiles) { + return tiles.filter(function(t) { + return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]); + }); + }; + + + // remove inflight requests that no longer cover the view.. + tile.removeInflightRequests = function(cache, tiles, callback, modifier) { + return _filter(cache.inflight, function(v, i) { + var wanted = _find(tiles, function(tile) { return i === tile.id + modifier; }); + if (!wanted) { + delete cache.inflight[i]; + } + return !wanted; + }).map(callback); // abort request + }; + + + tile.scaleExtent = function(val) { + if (!arguments.length) return _scaleExtent; + _scaleExtent = val; + return tile; + }; + + + tile.size = function(val) { + if (!arguments.length) return _size; + _size = val; + return tile; + }; + + + tile.scale = function(val) { + if (!arguments.length) return _scale; + _scale = val; + return tile; + }; + + + tile.translate = function(val) { + if (!arguments.length) return _translate; + _translate = val; + return tile; + }; + + + tile.zoomDelta = function(val) { + if (!arguments.length) return _zoomDelta; + _zoomDelta = +val; + return tile; + }; + + + // number to extend the rows/columns beyond those covering the viewport + tile.margin = function(val) { + if (!arguments.length) return _margin; + _margin = +val; + return tile; + }; + + + return tile; +} diff --git a/package.json b/package.json index 3f2f448fc..6148cea94 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "json-stringify-pretty-compact": "^1.1.0", "jsonschema": "^1.1.0", "mapillary-js": "2.12.1", - "mapillary_sprite_source": "^1.4.0", + "mapillary_sprite_source": "^1.5.0", "minimist": "^1.2.0", "mocha": "^5.0.0", "mocha-phantomjs-core": "^2.1.0", @@ -75,7 +75,7 @@ "osm-community-index": "0.4.5", "phantomjs-prebuilt": "~2.1.11", "request": "^2.85.0", - "rollup": "~0.60.0", + "rollup": "~0.63.2", "rollup-plugin-commonjs": "^9.0.0", "rollup-plugin-includepaths": "~0.2.3", "rollup-plugin-json": "^3.0.0", diff --git a/test/data/mvttest.pbf b/test/data/mvttest.pbf new file mode 100644 index 000000000..63858a7a9 Binary files /dev/null and b/test/data/mvttest.pbf differ diff --git a/test/index.html b/test/index.html index 602943c8d..b094f4b32 100644 --- a/test/index.html +++ b/test/index.html @@ -116,6 +116,7 @@ + diff --git a/test/spec/svg/mvt.js b/test/spec/svg/mvt.js new file mode 100644 index 000000000..e5ab522aa --- /dev/null +++ b/test/spec/svg/mvt.js @@ -0,0 +1,97 @@ +describe('iD.svgMvt', function () { + var context; + var surface; + var dispatch = d3.dispatch('change'); + var projection = iD.geoRawMercator() + .translate([6934098.868981334, 4092682.5519805425]) + .scale(iD.geoZoomToScale(17)) + .clipExtent([[0, 0], [1000, 1000]]); + + + var gj = { + 'type': 'FeatureCollection', + 'features': [ + { + 'type': 'Feature', + 'id': 316973311, + 'geometry': { + 'type': 'Point', + 'coordinates': [ + -74.38928604125977, + 40.150275473401365 + ] + }, + 'properties': { + 'abbr': 'N.J.', + 'area': 19717.8, + 'name': 'New Jersey', + 'name_en': 'New Jersey', + 'osm_id': 316973311 + } + } + ] + }; + + beforeEach(function () { + context = iD.coreContext(); + d3.select(document.createElement('div')) + .attr('id', 'map') + .call(context.map().centerZoom([-74.389286, 40.1502754], 17)); + + surface = context.surface(); + }); + + it('creates layer-mvt', function () { + var render = iD.svgMvt(projection, context, dispatch); + surface.call(render); + + var layers = surface.selectAll('g.layer-mvt').nodes(); + expect(layers.length).to.eql(1); + }); + + it('draws geojson', function () { + var render = iD.svgMvt(projection, context, dispatch).geojson(gj); + surface.call(render); + + var path = surface.selectAll('path.mvt'); + expect(path.nodes().length).to.eql(1); + expect(path.attr('d')).to.match(/^M.*z$/); + }); + + describe('#url', function() { + it('handles pbf url', function () { + var url = '../../data/mvttest.pbf'; + var render = iD.svgMvt(projection, context, dispatch).url(url); + surface.call(render); + + var path = surface.selectAll('path.mvt'); + expect(path.nodes().length).to.eql(1); + expect(path.attr('d')).to.match(/^M.*z$/); + }); + }); + + describe('#showLabels', function() { + it('shows labels by default', function () { + var render = iD.svgMvt(projection, context, dispatch).geojson(gj); + surface.call(render); + + var label = surface.selectAll('text.mvtlabel'); + expect(label.nodes().length).to.eql(1); + expect(label.text()).to.eql('New Jersey'); + + var halo = surface.selectAll('text.mvtlabel-halo'); + expect(halo.nodes().length).to.eql(1); + expect(halo.text()).to.eql('New Jersey'); + }); + + + it('hides labels with showLabels(false)', function () { + var render = iD.svgMvt(projection, context, dispatch).geojson(gj).showLabels(false); + surface.call(render); + + expect(surface.selectAll('text.mvtlabel').empty()).to.be.ok; + expect(surface.selectAll('text.mvtlabel-halo').empty()).to.be.ok; + }); + }); + +});