Files
iD/modules/ui/photoviewer.js
2025-05-15 17:03:48 +02:00

301 lines
12 KiB
JavaScript

import {
select as d3_select
} from 'd3-selection';
import { clamp } from 'lodash-es';
import { t } from '../core/localizer';
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { svgIcon } from '../svg/icon';
import { utilGetDimensions } from '../util/dimensions';
import { utilRebind } from '../util';
import { services } from '../services';
import { uiTooltip } from './tooltip';
import { actionChangeTags } from '../actions';
import { geoSphericalDistance } from '../geo';
export function uiPhotoviewer(context) {
var dispatch = d3_dispatch('resize');
var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';
const addPhotoIdButton = new Set(['mapillary', 'panoramax']);
function photoviewer(selection) {
selection
.append('button')
.attr('class', 'thumb-hide')
.attr('title', t('icons.close'))
.on('click', function () {
for (const service of Object.values(services)) {
if (typeof service.hideViewer === 'function') {
service.hideViewer(context);
}
}
})
.append('div')
.call(svgIcon('#iD-icon-close'));
function preventDefault(d3_event) {
d3_event.preventDefault();
}
selection
.append('button')
.attr('class', 'resize-handle-xy')
.on('touchstart touchdown touchend', preventDefault)
.on(
_pointerPrefix + 'down',
buildResizeListener(selection, 'resize', dispatch, { resizeOnX: true, resizeOnY: true })
);
selection
.append('button')
.attr('class', 'resize-handle-x')
.on('touchstart touchdown touchend', preventDefault)
.on(
_pointerPrefix + 'down',
buildResizeListener(selection, 'resize', dispatch, { resizeOnX: true })
);
selection
.append('button')
.attr('class', 'resize-handle-y')
.on('touchstart touchdown touchend', preventDefault)
.on(
_pointerPrefix + 'down',
buildResizeListener(selection, 'resize', dispatch, { resizeOnY: true })
);
// update sett_photo_from_viewer button on selection change and when tags change
context.features().on('change.setPhotoFromViewer', function() {
setPhotoTagButton();
});
context.history().on('change.setPhotoFromViewer', function() {
setPhotoTagButton();
});
function setPhotoTagButton() {
const service = getServiceId();
const isActiveForService = addPhotoIdButton.has(service) &&
services[service].isViewerOpen() &&
layerEnabled(service) &&
context.mode().id === 'select';
renderAddPhotoIdButton(service, isActiveForService);
function layerEnabled(which) {
const layers = context.layers();
const layer = layers.layer(which);
return layer.enabled();
}
function getServiceId() {
for (const serviceId in services) {
const service = services[serviceId];
if (typeof service.isViewerOpen === 'function') {
if (service.isViewerOpen()) {
return serviceId;
}
}
}
return false;
}
function renderAddPhotoIdButton(service, shouldDisplay) {
const button = selection.selectAll('.set-photo-from-viewer').data(shouldDisplay ? [0] : []);
button.exit()
.remove();
const buttonEnter = button.enter()
.append('button')
.attr('class', 'set-photo-from-viewer')
.call(svgIcon('#fas-eye-dropper'))
.call(uiTooltip()
.title(() => t.append('inspector.set_photo_from_viewer.enable'))
.placement('right')
);
buttonEnter.select('.tooltip')
.classed('dark', true)
.style('width', '300px')
.merge(button)
.on('click', function (e) {
e.preventDefault();
e.stopPropagation();
const activeServiceId = getServiceId();
const image = services[activeServiceId].getActiveImage();
const action = graph =>
context.selectedIDs().reduce((graph, entityID) => {
const tags = graph.entity(entityID).tags;
const action = actionChangeTags(entityID, {...tags, [activeServiceId]: image.id});
return action(graph);
}, graph);
const annotation = t('operations.change_tags.annotation');
context.perform(action, annotation);
buttonDisable('already_set');
});
if (service === 'panoramax') {
const panoramaxControls = selection.select('.panoramax-wrapper .pnlm-zoom-controls.pnlm-controls');
panoramaxControls
.style('margin-top', shouldDisplay ? '36px' : '6px');
}
if (!shouldDisplay) return;
const activeImage = services[service].getActiveImage();
const graph = context.graph();
const entities = context.selectedIDs()
.map(id => graph.hasEntity(id))
.filter(Boolean);
if (entities.map(entity => entity.tags[service])
.every(value => value === activeImage?.id)) {
buttonDisable('already_set');
} else if (activeImage && entities
.map(entity => entity.extent(context.graph()).center())
.every(loc => geoSphericalDistance(loc, activeImage.loc) > 100)) {
buttonDisable('too_far');
} else {
buttonDisable(false);
}
}
function buttonDisable(reason) {
const disabled = reason !== false;
const button = selection.selectAll('.set-photo-from-viewer').data([0]);
button.attr('disabled', disabled ? 'true' : null);
button.classed('disabled', disabled);
button.call(uiTooltip().destroyAny);
if (disabled) {
button.call(uiTooltip()
.title(() => t.append(`inspector.set_photo_from_viewer.disable.${reason}`))
.placement('right')
);
} else {
button.call(uiTooltip()
.title(() => t.append('inspector.set_photo_from_viewer.enable'))
.placement('right')
);
}
button.select('.tooltip')
.classed('dark', true)
.style('width', '300px');
}
}
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 pointerId;
var startX;
var startY;
var startWidth;
var startHeight;
function startResize(d3_event) {
if (pointerId !== (d3_event.pointerId || 'mouse')) return;
d3_event.preventDefault();
d3_event.stopPropagation();
var mapSize = context.map().dimensions();
if (resizeOnX) {
var mapWidth = mapSize[0];
const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-left'), 10);
var newWidth = clamp((startWidth + d3_event.clientX - startX), minWidth, mapWidth - viewerMargin * 2);
target.style('width', newWidth + 'px');
}
if (resizeOnY) {
const menuHeight = utilGetDimensions(d3_select('.top-toolbar'))[1] +
utilGetDimensions(d3_select('.map-footer'))[1];
const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-bottom'), 10);
var maxHeight = mapSize[1] - menuHeight - viewerMargin * 2; // 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, subtractPadding(utilGetDimensions(target, true), target));
}
function stopResize(d3_event) {
if (pointerId !== (d3_event.pointerId || 'mouse')) return;
d3_event.preventDefault();
d3_event.stopPropagation();
// remove all the listeners we added
d3_select(window)
.on('.' + eventName, null);
}
return function initResize(d3_event) {
d3_event.preventDefault();
d3_event.stopPropagation();
pointerId = d3_event.pointerId || 'mouse';
startX = d3_event.clientX;
startY = d3_event.clientY;
var targetRect = target.node().getBoundingClientRect();
startWidth = targetRect.width;
startHeight = targetRect.height;
d3_select(window)
.on(_pointerPrefix + 'move.' + eventName, startResize, false)
.on(_pointerPrefix + 'up.' + eventName, stopResize, false);
if (_pointerPrefix === 'pointer') {
d3_select(window)
.on('pointercancel.' + eventName, stopResize, false);
}
};
}
}
photoviewer.onMapResize = function() {
var photoviewer = context.container().select('.photoviewer');
var content = context.container().select('.main-content');
var mapDimensions = utilGetDimensions(content, true);
const menuHeight = utilGetDimensions(d3_select('.top-toolbar'))[1] +
utilGetDimensions(d3_select('.map-footer'))[1];
const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-bottom'), 10);
// shrink photo viewer if it is too big (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] - menuHeight - viewerMargin * 2)) {
var setPhotoDimensions = [
Math.min(photoDimensions[0], mapDimensions[0]),
Math.min(photoDimensions[1], mapDimensions[1] - menuHeight - viewerMargin * 2),
];
photoviewer
.style('width', setPhotoDimensions[0] + 'px')
.style('height', setPhotoDimensions[1] + 'px');
dispatch.call('resize', photoviewer, subtractPadding(setPhotoDimensions, photoviewer));
}
};
function subtractPadding(dimensions, selection) {
return [
dimensions[0] - parseFloat(selection.style('padding-left')) - parseFloat(selection.style('padding-right')),
dimensions[1] - parseFloat(selection.style('padding-top')) - parseFloat(selection.style('padding-bottom'))
];
}
return utilRebind(photoviewer, dispatch, 'on');
}