add list of loaded local photos

This commit is contained in:
Martin Raifer
2023-08-08 17:12:35 +02:00
parent 634ce10d24
commit 471354af4f
7 changed files with 244 additions and 70 deletions

View File

@@ -471,3 +471,98 @@ label.streetside-hires {
color: #fff;
}
}
/* local georeferenced photos */
.local-photos {
display: flex;
}
.local-photos > div {
width: 50%;
}
.local-photos > div:first-child {
margin-right: 20px;
}
.preview-local-photos {
max-height: 40vh;
overflow-y: scroll;
overflow-x: auto;
/* workaround for something like "overflow-x: visible"
see https://stackoverflow.com/a/39554003 */
margin-left: -100px;
padding-left: 100px;
}
.preview-local-photos::-webkit-scrollbar {
border-left: none;
}
.preview-local-photos li {
list-style: none;
display: flex;
justify-content: space-between;
height: 30px;
}
.preview-local-photos span.filename {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 30px;
padding-left: 8px;
border-bottom: 1px solid #ccc;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
}
.preview-local-photos li:first-child span.filename {
border-top: 1px solid #ccc;
border-top-left-radius: 4px;
}
.preview-local-photos li:first-child button {
border-top: 1px solid #ccc;
}
.preview-local-photos li:first-child button.remove {
border-top-right-radius: 4px;
}
.preview-local-photos li:last-child span.filename {
border-bottom-left-radius: 4px;
}
.preview-local-photos li:last-child button.remove {
border-bottom-right-radius: 4px;
}
.preview-local-photos li.invalid span.filename {
color: #ccc;
}
/*.preview-local-photos li.invalid span.filename::before {
content: "! ";
color: red;
}*/
.preview-local-photos li.invalid button.zoom-to-data {
display: none;
}
.preview-local-photos li button.no-geolocation {
display: none;
}
.preview-local-photos li.invalid button.no-geolocation {
display: block;
}
.preview-local-photos .placeholder div {
display: block;
height: 40px;
width: 40px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
background-image: url(img/loader-black.gif);
filter: invert(1);
}
.local-photos label.button {
background: #7092ff;
color: #fff;
font-weight: bold;
padding: 10px 25px;
text-align: center;
font-size: 12px;
display: inline-block;
border-radius: 4px;
cursor: pointer;
}

View File

@@ -5650,7 +5650,7 @@ li.hide + li.version .badge .tooltip .popover-arrow {
/* Scrollbars
----------------------------------------------------- */
::-webkit-scrollbar {
height: 20px;
height: 10px;
overflow: visible;
width: 10px;
border-left: 1px solid #DDD;
@@ -5677,6 +5677,9 @@ li.hide + li.version .badge .tooltip .popover-arrow {
background-color: rgba(0,0,0,.05);
}
}
body {
scrollbar-width: 10px;
}
/* Intro walkthrough

View File

@@ -1446,9 +1446,13 @@ en:
tooltip: Add georeferenced photos from local files
tooltip_edit: Edit georeferenced photos
header: Georeferenced Photos
zoom: Zoom to photos
zoom_single: Zoom to photo
file:
instructions: "Choose georeferenced photos to be displayed. Supported types are:\n .jpg with exif location data"
instructions: "Choose georeferenced photos to be displayed. Supported types are .jpg and .png with exif location data"
label: "Browse files"
no_geolocation:
tooltip: Image without geolocation cannot be located on the map
note:
note: Note
title: Edit note

View File

@@ -1,7 +1,7 @@
import { select as d3_select } from 'd3-selection';
import exifr from 'exifr';
import { utilDetect } from '../util/detect';
import { select as d3_select } from 'd3-selection';
import { geoExtent } from '../geo';
import { isArray, isNumber } from 'lodash-es';
@@ -12,7 +12,8 @@ export function svgLocalPhotos(projection, context, dispatch) {
var detected = utilDetect();
let layer = d3_select(null);
var _fileList;
var _imageList = [];
var _photos = [];
var _idAutoinc = 0;
function init() {
if (_initialized) return; // run once
@@ -45,7 +46,7 @@ export function svgLocalPhotos(projection, context, dispatch) {
}
// opens the image at bottom left
function click(d3_event, image) {
function click(d3_event, image, zoomTo) {
// removes old div(s), if any
closePhotoViewer();
@@ -71,7 +72,9 @@ export function svgLocalPhotos(projection, context, dispatch) {
// centers the map with image location
context.map().centerEase(image.loc);
if (zoomTo) {
context.map().centerEase(image.loc);
}
}
@@ -127,7 +130,7 @@ export function svgLocalPhotos(projection, context, dispatch) {
function drawPhotos(selection) {
layer = selection.selectAll('.layer-local-photos')
.data(_fileList ? [0] : []);
.data(_photos ? [0] : []);
layer.exit()
.remove();
@@ -143,20 +146,18 @@ export function svgLocalPhotos(projection, context, dispatch) {
layer = layerEnter
.merge(layer);
// if (_imageList.length !== 0) {
// if (_fileList && _fileList.length !== 0) {
if (_imageList && _imageList.length !== 0) {
display_markers(_imageList);
if (_photos && _photos.length !== 0) {
display_markers(_photos);
}
}
/**
* Reads and parses files
* @param {Array<object>} arrayFiles - Holds array of file - [file_1, file_2, ...]
* @param {Array<object>} files - Holds array of file - [file_1, file_2, ...]
*/
async function readmultifiles(arrayFiles) {
const filePromises = arrayFiles.map((file, i) => {
async function readmultifiles(files) {
const filePromises = files.map(file => {
// Return a promise per file
return new Promise((resolve, reject) => {
const reader = new FileReader();
@@ -167,8 +168,12 @@ export function svgLocalPhotos(projection, context, dispatch) {
try {
const response = await exifr.parse(file)
.then(output => {
_imageList.push({
id: i,
if (_photos.find(i => i.name === file.name && i.src === reader.result)) {
// skip if already loaded photos
return;
}
_photos.push({
id: _idAutoinc++,
name: file.name,
src: reader.result,
loc: [output.longitude, output.latitude]
@@ -177,10 +182,12 @@ export function svgLocalPhotos(projection, context, dispatch) {
// Resolve the promise with the response value
resolve(response);
} catch (err) {
console.error(err); // eslint-disable-line no-console
reject(err);
}
};
reader.onerror = (error) => {
console.error(err); // eslint-disable-line no-console
reject(error);
};
@@ -188,15 +195,14 @@ export function svgLocalPhotos(projection, context, dispatch) {
});
// Wait for all promises to be resolved
await Promise.all(filePromises);
await Promise.allSettled(filePromises);
_photos = _photos.sort((a, b) => a.id - b.id);
dispatch.call('change');
}
drawPhotos.setFile = function(fileList) {
// read and parse asynchronously
readmultifiles(Array.from(fileList));
dispatch.call('change');
return this;
};
@@ -219,8 +225,20 @@ export function svgLocalPhotos(projection, context, dispatch) {
return this;
};
drawPhotos.getPhotos = function() {
return _photos;
};
drawPhotos.removePhoto = function(id) {
_photos = _photos.filter(i => i.id !== id);
dispatch.call('change');
return _photos;
};
drawPhotos.openPhoto = click;
drawPhotos.fitZoom = function() {
let extent = _imageList
let extent = _photos
.map(image => image.loc)
.filter(l => isArray(l) && isNumber(l[0]) && isNumber(l[1]))
.map(l => geoExtent(l, l))
@@ -268,7 +286,7 @@ export function svgLocalPhotos(projection, context, dispatch) {
};
drawPhotos.hasData = function() {
return isArray(_imageList) && _imageList.length > 0;
return isArray(_photos) && _photos.length > 0;
};

View File

@@ -390,7 +390,7 @@ export function uiSectionPhotoOverlays(context) {
.append('button')
.attr('class', 'zoom-to-data')
.call(uiTooltip()
.title(() => t.append('map_data.layers.custom.zoom'))
.title(() => t.append('local_photos.zoom'))
.placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')
)
.on('click', function(d3_event) {
@@ -427,9 +427,7 @@ export function uiSectionPhotoOverlays(context) {
function localPhotosChanged(d) {
var localPhotosLayer = layers.layer('local-photos');
if (d && d.fileList) {
localPhotosLayer.fileList(d.fileList);
}
localPhotosLayer.fileList(d);
}
context.layers().on('change.uiSectionPhotoOverlays', section.reRender);

View File

@@ -292,7 +292,11 @@ export function uiSectionRawTagEditor(id, context) {
});
items.selectAll('button.remove')
.on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'down', removeTag); // 'click' fires too late - #5878
.on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'down', // 'click' fires too late - #5878
(d3_event, d) => {
if (d3_event.button !== 0) return;
removeTag(d3_event, d);
});
}

View File

@@ -1,87 +1,139 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import { t } from '../../core/localizer';
import { uiConfirm } from '../confirm';
import { utilRebind } from '../../util';
import { isArray, isNumber } from 'lodash-es';
import { uiTooltip } from '../tooltip';
import { svgIcon } from '../../svg';
export function uiSettingsLocalPhotos(context) {
var dispatch = d3_dispatch('change');
var photoLayer = context.layers().layer('local-photos');
var modal;
function render(selection) {
var dataLayer = context.layers().layer('local-photos');
var _currSettings = {
fileList: (dataLayer && dataLayer.fileList()) || null
};
// var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png';
var modal = uiConfirm(selection).okButton();
modal = uiConfirm(selection).okButton();
modal
.classed('settings-modal settings-custom-data', true);
.classed('settings-modal settings-local-photos', true);
modal.select('.modal-section.header')
.append('h3')
.call(t.append('local_photos.header'));
modal.select('.modal-section.message-text')
.append('div')
.classed('local-photos', true);
var textSection = modal.select('.modal-section.message-text');
var instructionsSection = modal.select('.modal-section.message-text .local-photos')
.append('div')
.classed('instructions', true);
textSection
.append('pre')
.attr('class', 'instructions-local-photos')
instructionsSection
.append('p')
.classed('instructions-local-photos', true)
.call(t.append('local_photos.file.instructions'));
textSection
instructionsSection
.append('input')
.attr('class', 'field-file')
.classed('field-file', true)
.attr('type', 'file')
.attr('multiple', 'multiple')
// .attr('accept', '.gpx,.kml,.geojson,.json,application/gpx+xml,application/vnd.google-earth.kml+xml,application/geo+json,application/json')
.property('files', _currSettings.fileList)
.attr('accept', '.jpg,.jpeg,.png,image/png,image/jpeg')
.style('visibility', 'hidden')
.attr('id', 'local-photo-files')
.on('change', function(d3_event) {
var files = d3_event.target.files;
if (files && files.length) {
_currSettings.fileList = files;
} else {
_currSettings.fileList = null;
previews
.select('ul')
.append('li')
.classed('placeholder', true)
.append('div');
dispatch.call('change', this, files);
}
d3_event.target.value = null;
});
instructionsSection
.append('label')
.attr('for', 'local-photo-files')
.classed('button', true)
.call(t.append('local_photos.file.label'));
const previews = modal.select('.modal-section.message-text .local-photos')
.append('div')
.append('div')
.classed('preview-local-photos', true)
// insert a cancel button
var buttonSection = modal.select('.modal-section.buttons');
previews
.append('ul');
buttonSection
.insert('button', '.ok-button')
.attr('class', 'button cancel-button secondary-action')
.call(t.append('confirm.cancel'));
updatePreviews(previews.select('ul'));
context.layers().on('change', () => updatePreviews(previews.select('ul')));
}
buttonSection.select('.cancel-button')
.on('click.cancel', clickCancel);
buttonSection.select('.ok-button')
.attr('disabled', isSaveDisabled)
.on('click.save', clickSave);
function isSaveDisabled() {
return null;
function updatePreviews(container) {
function locationUnavailable(d) {
return !(isArray(d.loc) && isNumber(d.loc[0]) && isNumber(d.loc[1]));
}
container.selectAll('li.placeholder').remove();
function clickCancel() {
this.blur();
modal.close();
}
let selection = container.selectAll('li')
.data(photoLayer.getPhotos() ?? [], d => d.id);
selection.exit()
.remove();
function clickSave() {
this.blur();
modal.close();
dispatch.call('change', this, _currSettings);
}
const selectionEnter = selection.enter()
.append('li');
selectionEnter
.append('span')
.classed('filename', true);
selectionEnter
.append('button')
.classed('form-field-button zoom-to-data', true)
.attr('title', t('local_photos.zoom_single'))
.call(svgIcon('#iD-icon-framed-dot'));
selectionEnter
.append('button')
.classed('form-field-button no-geolocation', true)
.call(svgIcon('#iD-icon-alert'))
.call(uiTooltip()
.title(() => t.append('local_photos.no_geolocation.tooltip'))
.placement('left')
);
selectionEnter
.append('button')
.classed('form-field-button remove', true)
.attr('title', t('icons.remove'))
.call(svgIcon('#iD-operation-delete'));
selection = selection.merge(selectionEnter);
selection
.classed('invalid', locationUnavailable);
selection.select('span.filename')
.text(d => d.name)
.attr('title', d => d.name);
selection.select('span.filename')
.on('click', (d3_event, d) => {
photoLayer.openPhoto(d3_event, d, false);
});
selection.select('button.zoom-to-data')
.on('click', (d3_event, d) => {
photoLayer.openPhoto(d3_event, d, true);
});
selection.select('button.remove')
.on('click', (d3_event, d) => {
photoLayer.removePhoto(d.id);
updatePreviews(container);
});
}
return utilRebind(render, dispatch, 'on');