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..21fc4efc6 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", @@ -773,6 +773,9 @@ "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", diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index 80340d790..e264d5cca 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -410,6 +410,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..99be60e12 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -34,17 +34,17 @@ import { } from '../util'; -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) { @@ -129,12 +129,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 +367,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/streetside.js b/modules/services/streetside.js index f13470b6c..040f66b6c 100644 --- a/modules/services/streetside.js +++ b/modules/services/streetside.js @@ -669,6 +669,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/package.json b/package.json index 3f2f448fc..4c914d053 100644 --- a/package.json +++ b/package.json @@ -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; + }); + }); + +});