diff --git a/.github/workflows/ant-release.yml b/.github/workflows/ant-release.yml index d428b0e2d..9691f856d 100644 --- a/.github/workflows/ant-release.yml +++ b/.github/workflows/ant-release.yml @@ -9,5 +9,5 @@ jobs: uses: JOSM/JOSMPluginAction/.github/workflows/ant.yml@v2 with: josm-revision: "r18877" - + java-version: 17 diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml index e46400f11..563dc424d 100644 --- a/.github/workflows/ant.yml +++ b/.github/workflows/ant.yml @@ -20,3 +20,5 @@ jobs: uses: JOSM/JOSMPluginAction/.github/workflows/ant.yml@v2 with: josm-revision: ${{ matrix.josm-revision }} + java-version: 17 + diff --git a/.github/workflows/reports.yaml b/.github/workflows/reports.yaml index 43794f4f3..817c66ff4 100644 --- a/.github/workflows/reports.yaml +++ b/.github/workflows/reports.yaml @@ -11,3 +11,4 @@ permissions: jobs: call-workflow: uses: JOSM/JOSMPluginAction/.github/workflows/reports.yaml@v2 + diff --git a/build.gradle.kts b/build.gradle.kts index 7c2db2961..11eb3834a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,8 +52,8 @@ tasks.withType(JavaCompile::class).configureEach { } } -java.sourceCompatibility = JavaVersion.VERSION_1_8 -java.targetCompatibility = JavaVersion.VERSION_1_8 +java.sourceCompatibility = JavaVersion.VERSION_17 +java.targetCompatibility = JavaVersion.VERSION_17 val versions = mapOf( "awaitility" to "4.2.0", diff --git a/build.xml b/build.xml index 009eecf8c..06eeec762 100644 --- a/build.xml +++ b/build.xml @@ -6,6 +6,8 @@ + + diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/OrganizationRecord.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/OrganizationRecord.java index 0953e5097..9797b76d9 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/OrganizationRecord.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/OrganizationRecord.java @@ -33,34 +33,27 @@ /** * Record organization information * + * @param id the unique key for the organization + * @param name the machine-readable name for an organization + * @param niceName the human-readable name for an organization + * @param description the description for the organization + * @param avatar the avatar for the organization * @author Taylor Smock */ -// @Immutable -public final class OrganizationRecord implements Serializable { +public record OrganizationRecord(long id, String name, String niceName, String description, ImageIcon avatar) + implements Serializable { + private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); private static final ListenerList LISTENERS = ListenerList.create(); - private final String description; - private final long id; - private final String name; - private final String niceName; - private final ImageIcon avatar; private static final Map CACHE = new ConcurrentHashMap<>(1); - public static final OrganizationRecord NULL_RECORD = new OrganizationRecord(0L, "", "", "", ""); + public static final OrganizationRecord NULL_RECORD = new OrganizationRecord(0L, "", "", "", createAvatarIcon("")); static { CACHE.put(0L, NULL_RECORD); } - private OrganizationRecord(Long id, String slug, String name, String description, String avatarUrl) { - this.avatar = createAvatarIcon(avatarUrl); - this.description = description; - this.id = id; - this.name = slug; - this.niceName = name; - } - /** * Get or create an avatar icon * @@ -84,9 +77,9 @@ private static ImageIcon createAvatarIcon(@Nullable String avatar) { */ private static BufferedImage fetchAvatarIcon(String url) { try { - HttpClient client = HttpClient.create(URI.create(url).toURL()); + final var client = HttpClient.create(URI.create(url).toURL()); OAuthUtils.addAuthenticationHeader(client); - HttpClient.Response response = client.connect(); + final var response = client.connect(); if (response.getResponseCode() >= 200 && response.getResponseCode() < 400) { return ImageIO.read(response.getContent()); } @@ -126,8 +119,8 @@ private static OrganizationRecord getNewOrganization(long id) { // TODO check for API in v4 (preferably one that doesn't need user auth) final String url = MapillaryConfig.getUrls().getOrganizationInformation(id); try { - final JsonObject data = OAuthUtils.getWithHeader(URI.create(url)); - final OrganizationRecord organizationRecord = decodeNewOrganization(data); + final var data = OAuthUtils.getWithHeader(URI.create(url)); + final var organizationRecord = decodeNewOrganization(data); // Ensure that we aren't blocking the main EDT thread MainApplication.worker.execute(() -> LISTENERS.fireEvent(l -> l.organizationAdded(organizationRecord))); return organizationRecord; @@ -144,16 +137,16 @@ private static OrganizationRecord getNewOrganization(long id) { * @return A new organization record */ private static OrganizationRecord decodeNewOrganization(JsonObject organization) { - String description = organization.getString("description", ""); - String slug = organization.getString("slug", ""); - String name = organization.getString("name", ""); - String idString = organization.getString("id", ""); - String avatarUrl = organization.getString("profile_photo_url", null); + final var description = organization.getString("description", ""); + final var slug = organization.getString("slug", ""); + final var name = organization.getString("name", ""); + final var idString = organization.getString("id", ""); + final var avatarUrl = organization.getString("profile_photo_url", null); long id = 0; if (NUMBER_PATTERN.matcher(idString).matches()) { id = Long.parseLong(idString); } - return new OrganizationRecord(id, slug, name, description, avatarUrl); + return new OrganizationRecord(id, slug, name, description, createAvatarIcon(avatarUrl)); } /** @@ -166,52 +159,6 @@ public static void addFromTile(MVTTile tile) { .forEach(MapillaryImageUtils::getOrganization); } - /** - * Get the avatar for the organization - * - * @return The avatar for the organization - */ - public ImageIcon getAvatar() { - return avatar != null ? avatar : ImageProvider.createBlankIcon(ImageSizes.DEFAULT); - } - - /** - * Get the description for the organization - * - * @return The organization description - */ - public String getDescription() { - return description; - } - - /** - * Get the unique key for the organization - * - * @return The organization key - */ - public long getId() { - return id; - } - - /** - * Get the machine-readable name for an organization - * - * @return The name of the organization - * @see OrganizationRecord#getNiceName - */ - public String getName() { - return name; - } - - /** - * Get the human-readable name for an organization - * - * @return The nice-looking name of the organization - */ - public String getNiceName() { - return niceName; - } - /** * Add listener for organizations * diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java index 1608c21ca..173b51117 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java @@ -12,15 +12,14 @@ import java.awt.event.ActionEvent; import java.awt.event.ItemEvent; import java.awt.event.KeyEvent; +import java.io.Serial; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collection; -import java.util.concurrent.locks.Lock; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -34,7 +33,6 @@ import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSeparator; -import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import jakarta.annotation.Nonnull; @@ -76,6 +74,7 @@ public final class MapillaryFilterDialog extends ToggleDialog implements OrganizationRecordListener, MVTTile.TileListener { + @Serial private static final long serialVersionUID = -4192029663670922103L; private static MapillaryFilterDialog instance; @@ -108,9 +107,9 @@ private MapillaryFilterDialog() { Shortcut.NONE), 200, false, MapillaryPreferenceSetting.class); - final JPanel panel = new JPanel(new GridBagLayout()); + final var panel = new JPanel(new GridBagLayout()); panel.add(new JLabel(tr("Picture Filters")), GBC.eol().anchor(GridBagConstraints.LINE_START)); - final JPanel imageLine = new JPanel(); + final var imageLine = new JPanel(); panel.add(imageLine, GBC.eol().anchor(GridBagConstraints.LINE_START)); this.addTimeFilters(panel); this.addUserGroupFilters(panel); @@ -120,7 +119,7 @@ private MapillaryFilterDialog() { panel.add(imageTypes, GBC.eol().anchor(GridBagConstraints.LINE_START)); panel.add(new JSeparator(), GBC.eol().fill(GridBagConstraints.HORIZONTAL)); - final TrafficSignFilter objectFilter = new TrafficSignFilter(); + final var objectFilter = new TrafficSignFilter(); panel.add(new JLabel(tr("Object Detection Filters")), GBC.eol().anchor(GridBagConstraints.WEST).fill(GridBagConstraints.HORIZONTAL)); this.destroyable.addListener(objectFilter); @@ -148,7 +147,7 @@ private MapillaryFilterDialog() { * @param panel The panel to add the filters to */ private void addUserGroupFilters(JPanel panel) { - final JPanel userSearchPanel = new JPanel(); + final var userSearchPanel = new JPanel(); userSearchPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); organizationLabel.setToolTipText(tr("Organizations")); @@ -181,23 +180,23 @@ private void addUserGroupFilters(JPanel panel) { */ private void addTimeFilters(JPanel panel) { // Time from panel - final JPanel fromPanel = new JPanel(); + final var fromPanel = new JPanel(); fromPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); - final JCheckBox filterByDateCheckbox = new JCheckBox(tr("Not older than: ")); + final var filterByDateCheckbox = new JCheckBox(tr("Not older than: ")); fromPanel.add(filterByDateCheckbox); - final SpinnerNumberModel spinnerModel = new SpinnerNumberModel(1.0, 0, 10000, .1); - final JSpinner spinner = new DisableShortcutsOnFocusGainedJSpinner(spinnerModel); + final var spinnerModel = new SpinnerNumberModel(1.0, 0, 10000, .1); + final var spinner = new DisableShortcutsOnFocusGainedJSpinner(spinnerModel); // Set the editor such that we aren't zooming all over the place. spinner.setEnabled(false); fromPanel.add(spinner); - final JComboBox time = new JComboBox<>(TIME_LIST); + final var time = new JComboBox<>(TIME_LIST); time.setEnabled(false); fromPanel.add(time); panel.add(fromPanel, GBC.eol().anchor(GridBagConstraints.LINE_START)); // Time panel - final JPanel timePanel = new JPanel(new GridBagLayout()); + final var timePanel = new JPanel(new GridBagLayout()); startDate = IDatePicker.getNewDatePicker(); endDate = IDatePicker.getNewDatePicker(); final Consumer> function = modified -> updateDates(startDate, endDate, modified); @@ -299,7 +298,7 @@ public void setEndDate(Instant end) { * @param organization The organization to filter on */ public void setOrganization(String organization) { - OrganizationRecord organizationRecord = OrganizationRecord.getOrganization(organization); + final var organizationRecord = OrganizationRecord.getOrganization(organization); this.organizations.setSelectedItem(organizationRecord); } @@ -307,10 +306,10 @@ public void setOrganization(String organization) { private static Instant convertDateRangeBox(@Nonnull SpinnerNumberModel spinner, @Nonnull JComboBox timeStep) { if (timeStep.isEnabled()) { - ZonedDateTime current = LocalDate.now(ZoneOffset.UTC).atStartOfDay(ZoneOffset.UTC); - String type = (String) timeStep.getSelectedItem(); - Number start = spinner.getNumber(); - int[] difference = new int[] { 0, 0, 0 }; // Year, Month, Day + final var current = LocalDate.now(ZoneOffset.UTC).atStartOfDay(ZoneOffset.UTC); + final var type = (String) timeStep.getSelectedItem(); + final var start = spinner.getNumber(); + final var difference = new int[] { 0, 0, 0 }; // Year, Month, Day if (TIME_LIST[0].equals(type)) { difference[0] = start.intValue(); difference[1] = (int) ((start.floatValue() - difference[0]) * 12); @@ -328,8 +327,8 @@ private static Instant convertDateRangeBox(@Nonnull SpinnerNumberModel spinner, } private static void updateDates(IDatePicker startDate, IDatePicker endDate, IDatePicker modified) { - Instant start = startDate.getInstant(); - Instant end = endDate.getInstant(); + final var start = startDate.getInstant(); + final var end = endDate.getInstant(); if (Instant.MIN.equals(start) || Instant.MIN.equals(end)) { return; } @@ -388,7 +387,7 @@ public synchronized void refresh() { public void updateFilteredImages() { if (MapillaryLayer.hasInstance()) { MainApplication.worker.execute(() -> { - final Lock readLock = MapillaryLayer.getInstance().getData().getReadLock(); + final var readLock = MapillaryLayer.getInstance().getData().getReadLock(); try { readLock.lockInterruptibly(); this.updateFilteredImages(MapillaryLayer.getInstance().getData().getNodes()); @@ -485,7 +484,7 @@ public boolean test(INode img, Collection currentSelection) { // Filter on organizations return !OrganizationRecord.NULL_RECORD.equals(this.organization) && MapillaryImageUtils.getSequenceKey(img) != null - && this.organization.getId() != MapillaryImageUtils.getOrganization(img).getId(); + && this.organization.id() != MapillaryImageUtils.getOrganization(img).id(); } return false; } @@ -505,9 +504,9 @@ private boolean checkStartDate(INode img) { if (Instant.MIN.equals(startDateRefresh)) { return false; } - final Instant start = LocalDateTime.ofInstant(startDateRefresh, ZoneOffset.UTC).toLocalDate() + final var start = LocalDateTime.ofInstant(startDateRefresh, ZoneOffset.UTC).toLocalDate() .atStartOfDay(ZoneOffset.UTC).toInstant(); - final Instant imgDate = MapillaryImageUtils.getDate(img); + final var imgDate = MapillaryImageUtils.getDate(img); return start.isAfter(imgDate); } @@ -519,10 +518,10 @@ private boolean checkEndDate(INode img) { if (Instant.MIN.equals(endDateRefresh)) { return false; } - final ZonedDateTime nextDate = LocalDateTime.ofInstant(endDateRefresh, ZoneOffset.UTC).toLocalDate() + final var nextDate = LocalDateTime.ofInstant(endDateRefresh, ZoneOffset.UTC).toLocalDate() .atStartOfDay(ZoneOffset.UTC).plus(1, ChronoUnit.DAYS); - final Instant end = nextDate.toInstant(); - final Instant imgDate = MapillaryImageUtils.getDate(img); + final var end = nextDate.toInstant(); + final var imgDate = MapillaryImageUtils.getDate(img); return end.isBefore(imgDate); } } @@ -536,6 +535,7 @@ public static synchronized void destroyInstance() { private static class UpdateAction extends AbstractAction { + @Serial private static final long serialVersionUID = -7417238601979689863L; UpdateAction() { @@ -550,6 +550,7 @@ public void actionPerformed(ActionEvent arg0) { } private static class ResetAction extends AbstractAction { + @Serial private static final long serialVersionUID = 1178261778165525040L; ResetAction() { @@ -579,8 +580,8 @@ public void destroy() { @Override public void organizationAdded(OrganizationRecord organization) { - boolean add = true; - for (int i = 0; i < organizations.getItemCount(); i++) { + var add = true; + for (var i = 0; i < organizations.getItemCount(); i++) { if (organizations.getItemAt(i).equals(organization)) { add = false; break; diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/OrganizationListCellRenderer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/OrganizationListCellRenderer.java index 0a31b21f9..21d3e8243 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/OrganizationListCellRenderer.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/OrganizationListCellRenderer.java @@ -1,3 +1,4 @@ +// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.mapillary.gui.dialog; import java.awt.Component; @@ -27,13 +28,13 @@ public Component getListCellRendererComponent(JList list, Object value, int i JLabel comp = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if (value instanceof OrganizationRecord) { OrganizationRecord organization = (OrganizationRecord) value; - if ((organization.getNiceName() != null && !organization.getNiceName().isEmpty()) + if ((organization.niceName() != null && !organization.niceName().isEmpty()) || OrganizationRecord.NULL_RECORD.equals(organization)) { - comp.setText(organization.getNiceName()); + comp.setText(organization.niceName()); } else { - comp.setText(Long.toString(organization.getId())); + comp.setText(Long.toString(organization.id())); } - if (organization.getAvatar() != null) { + if (organization.avatar() != null) { comp.setIcon(ORGANIZATION_SCALED_ICONS.computeIfAbsent(organization, OrganizationListCellRenderer::scaleOrganizationIcon)); } @@ -49,7 +50,7 @@ public Component getListCellRendererComponent(JList list, Object value, int i */ private static ImageIcon scaleOrganizationIcon(final OrganizationRecord organization) { final ImageProvider.ImageSizes size = ImageProvider.ImageSizes.DEFAULT; - final Image scaledImage = organization.getAvatar().getImage().getScaledInstance(size.getAdjustedWidth(), + final Image scaledImage = organization.avatar().getImage().getScaledInstance(size.getAdjustedWidth(), size.getAdjustedHeight(), Image.SCALE_SMOOTH); return new ImageIcon(scaledImage); } diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/imageinfo/ImageInfoPanel.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/imageinfo/ImageInfoPanel.java index 6facb9edd..7d4989d7a 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/imageinfo/ImageInfoPanel.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/imageinfo/ImageInfoPanel.java @@ -11,6 +11,7 @@ import java.awt.datatransfer.StringSelection; import java.awt.event.KeyEvent; import java.awt.image.BufferedImage; +import java.io.Serial; import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; @@ -47,11 +48,13 @@ import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer; import org.openstreetmap.josm.plugins.mapillary.gui.widget.DisableShortcutsOnFocusGainedJSpinner; import org.openstreetmap.josm.plugins.mapillary.model.ImageDetection; +import org.openstreetmap.josm.plugins.mapillary.model.UserProfile; import org.openstreetmap.josm.plugins.mapillary.spi.preferences.MapillaryConfig; import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryImageUtils; import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties; import org.openstreetmap.josm.plugins.mapillary.utils.OffsetUtils; import org.openstreetmap.josm.tools.GBC; +import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.JosmRuntimeException; import org.openstreetmap.josm.tools.Logging; import org.openstreetmap.josm.tools.Shortcut; @@ -61,6 +64,7 @@ * A panel to show image specific information */ public final class ImageInfoPanel extends ToggleDialog implements DataSelectionListener, VectorDataSelectionListener { + @Serial private static final long serialVersionUID = 1320443250226377651L; private static ImageInfoPanel instance; private static final ImageIcon EMPTY_USER_AVATAR = new ImageIcon( @@ -69,6 +73,7 @@ public final class ImageInfoPanel extends ToggleDialog implements DataSelectionL private final JLabel numDetectionsLabel; private final JCheckBox showDetectionsCheck; private final JLabel usernameLabel; + private final JLabel organizationNameLabel; private final HtmlPanel imgKeyValue; private final WebLinkAction imgLinkAction; private final ClipboardAction copyImgUrlAction; @@ -109,30 +114,33 @@ private ImageInfoPanel() { usernameLabel = new JLabel(); usernameLabel.setFont(usernameLabel.getFont().deriveFont(Font.PLAIN)); + organizationNameLabel = new JLabel(); + organizationNameLabel.setFont(organizationNameLabel.getFont().deriveFont(Font.PLAIN)); + imgKeyValue = new HtmlPanel(); imgLinkAction = new WebLinkAction(tr("View in browser"), null); copyImgUrlAction = new ClipboardAction(tr("Copy URL"), tr("Copied URL to clipboard …"), null); - final MapillaryButton copyUrlButton = new MapillaryButton(copyImgUrlAction, true); + final var copyUrlButton = new MapillaryButton(copyImgUrlAction, true); copyImgUrlAction.setPopupParent(copyUrlButton); copyImgKeyAction = new ClipboardAction(tr("Copy key"), tr("Copied key to clipboard …"), null); - final MapillaryButton copyKeyButton = new MapillaryButton(copyImgKeyAction, true); + final var copyKeyButton = new MapillaryButton(copyImgKeyAction, true); copyImgKeyAction.setPopupParent(copyKeyButton); addMapillaryTagAction = new AddTagToPrimitiveAction(tr("Add Mapillary tag")); - JPanel imgKey = new JPanel(); + final var imgKey = new JPanel(); imgKey.add(imgKeyValue); imgKey.add(copyKeyButton); - JPanel imgButtons = new JPanel(new GridBagLayout()); + final var imgButtons = new JPanel(new GridBagLayout()); imgButtons.add(new MapillaryButton(imgLinkAction, true), GBC.eol()); imgButtons.add(copyUrlButton, GBC.eol()); imgButtons.add(new MapillaryButton(addMapillaryTagAction, true), GBC.eol()); seqKeyValue = new HtmlPanel(); - JPanel offsetPanel = new JPanel(); + final var offsetPanel = new JPanel(); offsetModel = new SpinnerNumberModel(OffsetUtils.getOffset(null), -100, 100, 1); offsetModel.addChangeListener(l -> { OffsetUtils.setOffset(offsetModel.getNumber()); @@ -142,8 +150,8 @@ private ImageInfoPanel() { }); offsetPanel.add(new DisableShortcutsOnFocusGainedJSpinner(offsetModel)); - JPanel root = new JPanel(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); + final var root = new JPanel(new GridBagLayout()); + final var gbc = new GridBagConstraints(); gbc.insets = new Insets(0, 5, 0, 5); // Left column @@ -155,6 +163,8 @@ private ImageInfoPanel() { root.add(new JLabel(tr("Image detections")), gbc); gbc.gridy += 2; gbc.gridheight = 1; + root.add(new JLabel(tr("User")), gbc); + gbc.gridy++; root.add(new JLabel(tr("Organization")), gbc); gbc.gridy++; root.add(new JLabel(tr("Image actions")), gbc); @@ -176,6 +186,8 @@ private ImageInfoPanel() { gbc.gridy++; root.add(usernameLabel, gbc); gbc.gridy++; + root.add(organizationNameLabel, gbc); + gbc.gridy++; root.add(imgButtons, gbc); gbc.gridy++; root.add(imgKey, gbc); @@ -250,7 +262,7 @@ private void selectedImageChanged(@Nullable INode oldImage, @Nullable INode newI final String newImageKey = newImage != null ? Long.toString(newImage.getId()) : null; if (newImageKey != null) { final boolean blur = Boolean.TRUE.equals(MapillaryProperties.IMAGE_LINK_TO_BLUR_EDITOR.get()); - final URI newImageUrl = blur ? MapillaryConfig.getUrls().blurEditImage(newImageKey) + final var newImageUrl = blur ? MapillaryConfig.getUrls().blurEditImage(newImageKey) : MapillaryConfig.getUrls().browseImage(newImageKey); offsetModel.setValue(OffsetUtils.getOffset(newImage)); @@ -258,7 +270,7 @@ private void selectedImageChanged(@Nullable INode oldImage, @Nullable INode newI imageLinkChangeListener = b -> imgLinkAction.setURI(newImageUrl); } else { try { - final URI newImageUrlWithLocation = new URI(newImageUrl.getScheme(), newImageUrl.getAuthority(), + final var newImageUrlWithLocation = new URI(newImageUrl.getScheme(), newImageUrl.getAuthority(), newImageUrl.getPath(), newImageUrl.getQuery() + "&z=18&lat=" + newImage.lat() + "&lng=" + newImage.lon(), newImageUrl.getFragment()); @@ -299,17 +311,46 @@ private void selectedImageChanged(@Nullable INode oldImage, @Nullable INode newI addMapillaryTagAction.setTag(null); } - final OrganizationRecord organizationRecord = MapillaryImageUtils.getOrganization(newImage); - usernameLabel.setEnabled(!OrganizationRecord.NULL_RECORD.equals(organizationRecord)); + setUserLabel(this.usernameLabel, MapillaryImageUtils.getUser(newImage)); + setOrganizationLabel(this.organizationNameLabel, MapillaryImageUtils.getOrganization(newImage)); + setSequenceKey(this.seqKeyValue, newImage); + } + + private static void setUserLabel(JLabel usernameLabel, UserProfile userProfile) { + usernameLabel.setEnabled(!UserProfile.NONE.equals(userProfile)); if (usernameLabel.isEnabled()) { - usernameLabel.setText(organizationRecord.getNiceName()); - usernameLabel.setIcon( - new ImageIcon(organizationRecord.getAvatar().getImage().getScaledInstance(32, 32, Image.SCALE_SMOOTH))); + usernameLabel.setText(userProfile.username()); + if (userProfile.avatar() != null) { + final var avatar = userProfile.avatar(); + final var expectedSize = ImageProvider.ImageSizes.DEFAULT; + final var height = expectedSize.getAdjustedHeight(); + final var width = expectedSize.getAdjustedWidth(); + if (avatar.getIconWidth() > width || avatar.getIconHeight() > height) { + usernameLabel.setIcon( + new ImageIcon(avatar.getImage().getScaledInstance(width, height, Image.SCALE_DEFAULT))); + } else { + usernameLabel.setIcon(userProfile.avatar()); + } + } } else { - usernameLabel.setText("‹" + tr("unknown organization") + "›"); + usernameLabel.setText("‹" + tr("unknown user") + "›"); usernameLabel.setIcon(EMPTY_USER_AVATAR); } + } + + private static void setOrganizationLabel(JLabel organizationNameLabel, OrganizationRecord organizationRecord) { + organizationNameLabel.setEnabled(!OrganizationRecord.NULL_RECORD.equals(organizationRecord)); + if (organizationNameLabel.isEnabled()) { + organizationNameLabel.setText(organizationRecord.niceName()); + organizationNameLabel.setIcon( + new ImageIcon(organizationRecord.avatar().getImage().getScaledInstance(32, 32, Image.SCALE_SMOOTH))); + } else { + organizationNameLabel.setText("‹" + tr("unknown organization") + "›"); + organizationNameLabel.setIcon(EMPTY_USER_AVATAR); + } + } + private static void setSequenceKey(HtmlPanel seqKeyValue, INode newImage) { final boolean partOfSequence = MapillaryImageUtils.getSequenceKey(newImage) != null; seqKeyValue.setEnabled(partOfSequence); if (partOfSequence) { diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java index 58a4e57e6..660244185 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java @@ -651,7 +651,7 @@ private void selectedImageChanged( if (newImage != null && !ImageDetection.getDetections(newImage.getId()).isEmpty()) { final long key = newImage.getId(); final Collection nodes = ImageDetection.getDetections(key).stream() - .map(detection -> data.getPrimitiveById(detection.getKey(), OsmPrimitiveType.NODE)) + .map(detection -> data.getPrimitiveById(detection.key(), OsmPrimitiveType.NODE)) .collect(Collectors.toList()); if (!nodes.containsAll(currentSelection) && !nodes.isEmpty() diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/geoimage/MapillaryImageEntry.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/geoimage/MapillaryImageEntry.java index 5b7e0df6f..e5d160709 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/geoimage/MapillaryImageEntry.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/geoimage/MapillaryImageEntry.java @@ -1,15 +1,12 @@ // License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.mapillary.gui.layer.geoimage; -import static org.openstreetmap.josm.tools.I18n.marktr; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.BasicStroke; -import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Rectangle; -import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; @@ -38,8 +35,6 @@ import com.drew.imaging.jpeg.JpegMetadataReader; import com.drew.imaging.jpeg.JpegProcessingException; -import com.drew.metadata.Directory; -import com.drew.metadata.Metadata; import com.drew.metadata.MetadataException; import com.drew.metadata.exif.ExifDirectoryBase; import com.drew.metadata.exif.ExifIFD0Directory; @@ -68,6 +63,7 @@ import org.openstreetmap.josm.plugins.mapillary.gui.layer.PointObjectLayer; import org.openstreetmap.josm.plugins.mapillary.model.ImageDetection; import org.openstreetmap.josm.plugins.mapillary.model.KeyIndexedObject; +import org.openstreetmap.josm.plugins.mapillary.model.UserProfile; import org.openstreetmap.josm.plugins.mapillary.spi.preferences.MapillaryConfig; import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryImageUtils; import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties; @@ -89,7 +85,6 @@ public class MapillaryImageEntry implements IImageEntry, BiConsumer>> { private static final CacheAccess CACHE = JCSCacheManager .getCache("mapillary:mapillaryimageentry"); - private static final String BASE_TITLE = marktr("Mapillary image"); private static final String MESSAGE_SEPARATOR = " — "; private final INode image; private final List> imageDetections = new ArrayList<>(); @@ -202,8 +197,8 @@ public MapillaryImageEntry getLastImage() { @Override public void selectImage(ImageViewerDialog imageViewerDialog, IImageEntry entry) { IImageEntry.super.selectImage(imageViewerDialog, entry); - if (entry instanceof MapillaryImageEntry) { - selectImage((MapillaryImageEntry) entry); + if (entry instanceof MapillaryImageEntry mapillaryImageEntry) { + selectImage(mapillaryImageEntry); } } @@ -216,29 +211,48 @@ private static void selectImage(@Nullable final MapillaryImageEntry entry) { @Override public String getDisplayName() { - StringBuilder title = new StringBuilder(tr(BASE_TITLE)); - if (MapillaryImageUtils.getKey(this.image) != 0) { - INode mapillaryImage = this.image; - OrganizationRecord organizationRecord = MapillaryImageUtils.getOrganization(mapillaryImage); - if (!OrganizationRecord.NULL_RECORD.equals(organizationRecord)) { - title.append(MESSAGE_SEPARATOR).append(organizationRecord.getNiceName()); - } - if (!Instant.EPOCH.equals(MapillaryImageUtils.getDate(mapillaryImage))) { - final boolean showHour = Boolean.TRUE.equals(MapillaryProperties.DISPLAY_HOUR.get()); - final Instant pictureTime = MapillaryImageUtils.getDate(mapillaryImage); + final var title = new StringBuilder(); + INode mapillaryImage = this.image; + if (MapillaryImageUtils.getKey(mapillaryImage) != 0) { + addUserInformation(mapillaryImage, title); + addOrganizationInformation(mapillaryImage, title); + addTimeInformation(mapillaryImage, title); + } + return title.toString(); + } + + private static void addUserInformation(INode mapillaryImage, StringBuilder title) { + final var userProfile = MapillaryImageUtils.getUser(mapillaryImage); + if (!UserProfile.NONE.equals(userProfile)) { + title.append(userProfile.username()); + } + } + + private static void addOrganizationInformation(INode mapillaryImage, StringBuilder title) { + final var organizationRecord = MapillaryImageUtils.getOrganization(mapillaryImage); + if (!OrganizationRecord.NULL_RECORD.equals(organizationRecord)) { + if (title.length() != 0) title.append(MESSAGE_SEPARATOR); - final DateFormat formatter; - if (showHour) { - formatter = DateUtils.getDateTimeFormat(DateFormat.DEFAULT, DateFormat.DEFAULT); - } else { - formatter = DateUtils.getDateFormat(DateFormat.DEFAULT); - } - // Use UTC, since mappers may be outside of "their" timezone, which would be even more confusing. - formatter.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)); - title.append(formatter.format(Date.from(pictureTime))); + title.append(organizationRecord.niceName()); + } + } + + private static void addTimeInformation(INode mapillaryImage, StringBuilder title) { + if (!Instant.EPOCH.equals(MapillaryImageUtils.getDate(mapillaryImage))) { + final boolean showHour = Boolean.TRUE.equals(MapillaryProperties.DISPLAY_HOUR.get()); + final Instant pictureTime = MapillaryImageUtils.getDate(mapillaryImage); + if (title.length() != 0) + title.append(MESSAGE_SEPARATOR); + final DateFormat formatter; + if (showHour) { + formatter = DateUtils.getDateTimeFormat(DateFormat.DEFAULT, DateFormat.DEFAULT); + } else { + formatter = DateUtils.getDateFormat(DateFormat.DEFAULT); } + // Use UTC, since mappers may be outside of "their" timezone, which would be even more confusing. + formatter.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)); + title.append(formatter.format(Date.from(pictureTime))); } - return title.toString(); } @Override @@ -246,8 +260,7 @@ public BufferedImage read(Dimension target) throws IOException { if (SwingUtilities.isEventDispatchThread()) { throw new JosmRuntimeException(tr("Mapillary image read should never occur on UI thread")); } - BufferedImageCacheEntry bufferedImageCacheEntry = Optional.ofNullable(this.originalImage) - .map(SoftReference::get).orElse(null); + var bufferedImageCacheEntry = Optional.ofNullable(this.originalImage).map(SoftReference::get).orElse(null); boolean tFullImage = this.fullImage; CompletableFuture bestForMemory; if (tFullImage) { @@ -355,7 +368,7 @@ private void drawDetections() throws IOException { final int height = bufferedLayeredImage.getHeight(); List detectionLayers = MainApplication.getLayerManager() .getLayersOfType(PointObjectLayer.class); - final AffineTransform unit2CompTransform = AffineTransform.getTranslateInstance(0, 0); + final var unit2CompTransform = AffineTransform.getTranslateInstance(0, 0); unit2CompTransform.concatenate(AffineTransform.getScaleInstance(width, height)); final Graphics2D graphics = bufferedLayeredImage.createGraphics(); @@ -376,9 +389,9 @@ private void drawDetections() throws IOException { || !checkIfDetectionInImageAndSelected(detectionLayers, imageDetection))) { continue; } - final Color color = imageDetection.getColor(); + final var color = imageDetection.getColor(); graphics.setColor(color); - final Shape transformedShape = unit2CompTransform.createTransformedShape(imageDetection.getShape()); + final var transformedShape = unit2CompTransform.createTransformedShape(imageDetection.getShape()); graphics.draw(transformedShape); ImageIcon icon = imageDetection.getValue().getIcon(); if (imageDetection.isTrafficSign() && !icon.equals(ObjectDetections.NO_ICON)) { @@ -405,7 +418,7 @@ private static boolean checkIfDetectionInImageAndSelected(List .map(VectorDataSet::getSelected).flatMap(Collection::stream).mapToLong(IPrimitive::getId) .mapToObj(l -> ImageDetection.getDetections(l, ImageDetection.Options.WAIT)).flatMap(Collection::stream) .collect(Collectors.toSet()); - return selectedDetections.stream().mapToLong(KeyIndexedObject::getKey).anyMatch(l -> l == detection.getKey()); + return selectedDetections.stream().mapToLong(KeyIndexedObject::key).anyMatch(l -> l == detection.key()); } @Override @@ -424,7 +437,7 @@ public File getFile() { return new File(getImageURI()); } - // @Override -- added in JOSM r18427 + @Override public URI getImageURI() { return MapillaryConfig.getUrls().browseImage(Long.toString(MapillaryImageUtils.getKey(this.image))); } @@ -589,7 +602,7 @@ public boolean equals(Object obj) { private void updateImageEntry() { // Clone this entry. Needed to ensure that the image display refreshes. - final MapillaryImageEntry temporaryImageEntry = new MapillaryImageEntry(this); + final var temporaryImageEntry = new MapillaryImageEntry(this); // Ensure that detections are repainted if (this.layeredImage != null) { this.layeredImage.clear(); @@ -604,15 +617,15 @@ private void updateImageEntry() { // FIXME copied from ImageEntry private BufferedImage applyRotation(BufferedImage img) { - int currentExifOrientation = getExifOrientation(); + final int currentExifOrientation = getExifOrientation(); if (!ExifReader.orientationNeedsCorrection(currentExifOrientation)) { return img; } - boolean switchesDimensions = ExifReader.orientationSwitchesDimensions(currentExifOrientation); - int width = switchesDimensions ? img.getHeight() : img.getWidth(); - int height = switchesDimensions ? img.getWidth() : img.getHeight(); - BufferedImage rotated = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - AffineTransform transform = ExifReader.getRestoreOrientationTransform(currentExifOrientation, img.getWidth(), + final boolean switchesDimensions = ExifReader.orientationSwitchesDimensions(currentExifOrientation); + final int width = switchesDimensions ? img.getHeight() : img.getWidth(); + final int height = switchesDimensions ? img.getWidth() : img.getHeight(); + final var rotated = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + final var transform = ExifReader.getRestoreOrientationTransform(currentExifOrientation, img.getWidth(), img.getHeight()); Graphics2D g = rotated.createGraphics(); g.drawImage(img, transform, null); @@ -627,15 +640,13 @@ private BufferedImage applyRotation(BufferedImage img) { */ private void updateExifInformation(byte[] imageBytes) { try { - final Metadata metadata = JpegMetadataReader.readMetadata(new ByteArrayInputStream(imageBytes)); - final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); - try { - if (dirExif != null && dirExif.containsTag(ExifDirectoryBase.TAG_ORIENTATION)) { - setExifOrientation(dirExif.getInt(ExifDirectoryBase.TAG_ORIENTATION)); - } - } catch (MetadataException ex) { - Logging.debug(ex); + final var metadata = JpegMetadataReader.readMetadata(new ByteArrayInputStream(imageBytes)); + final var dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + if (dirExif != null && dirExif.containsTag(ExifDirectoryBase.TAG_ORIENTATION)) { + setExifOrientation(dirExif.getInt(ExifDirectoryBase.TAG_ORIENTATION)); } + } catch (MetadataException e) { + Logging.debug(e); } catch (JpegProcessingException | IOException e) { Logging.error(e); } diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/ImageDetection.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/ImageDetection.java index 2237d36b7..7335002c0 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/ImageDetection.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/ImageDetection.java @@ -243,7 +243,7 @@ public Color getColor() { if (MainApplication.getLayerManager().getLayersOfType(PointObjectLayer.class).parallelStream() .map(PointObjectLayer::getData).map(VectorDataSet::getSelected).flatMap(Collection::stream) .mapToLong(IPrimitive::getId).mapToObj(ImageDetection::getDetections).flatMap(Collection::stream) - .filter(Objects::nonNull).anyMatch(id -> id.getKey().equals(this.getKey()))) { + .filter(Objects::nonNull).anyMatch(id -> id.key().equals(this.key()))) { return isRejected() || Boolean.TRUE.equals(MapillaryProperties.SMART_EDIT.get()) ? Color.RED : Color.CYAN; } if (this.isTrafficSign()) diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/KeyIndexedObject.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/KeyIndexedObject.java index 62bd21561..1852e5324 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/KeyIndexedObject.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/KeyIndexedObject.java @@ -5,53 +5,14 @@ /** * An object that is identified amongst objects of the same class by a {@link String} key. + * + * @param The key type */ -public class KeyIndexedObject implements Serializable { - private final T key; - - protected KeyIndexedObject(final T key) { - if (key == null) { - throw new IllegalArgumentException(); - } - this.key = key; - } - +public interface KeyIndexedObject extends Serializable { /** * Get the key for the object * * @return the unique key that identifies this object among other instances of the same class */ - public T getKey() { - return key; - } - - /* - * (non-Javadoc) - * @see java.lang.Object#hashCode() - */ - @Override - public int hashCode() { - final int prime = 31; - return prime * (prime + getClass().getName().hashCode()) + key.hashCode(); - } - - /* - * (non-Javadoc) - * @see java.lang.Object#equals(java.lang.Object) - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof KeyIndexedObject)) { - return false; - } - KeyIndexedObject other = (KeyIndexedObject) obj; - return key.equals(other.key); - } - + T key(); } diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/SpecialImageArea.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/SpecialImageArea.java index ab35c43fa..1a4f97ac7 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/SpecialImageArea.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/SpecialImageArea.java @@ -6,12 +6,19 @@ import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties; -public class SpecialImageArea extends KeyIndexedObject { +/** + * Special image areas + * + * @param The key type + * @param The shape type + */ +public class SpecialImageArea implements KeyIndexedObject { + private final S key; private final S imageKey; private final T shape; protected SpecialImageArea(final T shape, final S imageKey, final S key) { - super(key); + this.key = key; this.shape = shape; this.imageKey = imageKey; } @@ -27,9 +34,14 @@ public Shape getShape() { return shape; } + @Override + public S key() { + return this.key; + } + @Override public boolean equals(Object object) { - if (super.equals(object) && object instanceof SpecialImageArea) { + if (super.equals(object) && this.getClass().equals(object.getClass())) { SpecialImageArea other = (SpecialImageArea) object; return Objects.equals(this.shape, other.shape) && Objects.equals(this.imageKey, other.imageKey); } diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/UserProfile.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/UserProfile.java index 08cb2e3ee..6bfc59dac 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/UserProfile.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/model/UserProfile.java @@ -1,48 +1,61 @@ // License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.mapillary.model; -import java.util.Objects; +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import javax.swing.ImageIcon; +import jakarta.json.Json; +import org.openstreetmap.josm.plugins.mapillary.oauth.OAuthUtils; +import org.openstreetmap.josm.plugins.mapillary.spi.preferences.MapillaryConfig; +import org.openstreetmap.josm.plugins.mapillary.utils.api.JsonUserProfileDecoder; import org.openstreetmap.josm.tools.ImageProvider; - -public class UserProfile extends KeyIndexedObject { - private static final long serialVersionUID = -2626823438368139952L; - +import org.openstreetmap.josm.tools.Logging; + +/** + * A profile for a user + * + * @param key The user id + * @param username The username for the user + * @param avatar The avatar for the user + */ +public record UserProfile(long key, String username, ImageIcon avatar) { + + private static final Map CACHE = new ConcurrentHashMap<>(1); /** A default user profile */ - public static final UserProfile NONE = new UserProfile("", "", + public static final UserProfile NONE = new UserProfile(Long.MIN_VALUE, "", ImageProvider.createBlankIcon(ImageProvider.ImageSizes.DEFAULT)); - private final String username; - private final ImageIcon avatar; - - public UserProfile(String key, String username, ImageIcon avatar) { - super(key); - this.avatar = avatar; - this.username = username; + static { + CACHE.put(NONE.key(), NONE); } - public String getUsername() { - return username; - } - - public ImageIcon getAvatar() { - return avatar; + public static UserProfile getUser(String json) { + final UserProfile user; + try (var reader = Json.createReader(new StringReader(json))) { + user = JsonUserProfileDecoder.decodeUserProfile(reader.readObject()); + } + return CACHE.computeIfAbsent(user.key(), ignored -> user); } - @Override - public boolean equals(Object o) { - if (o instanceof UserProfile) { - UserProfile other = (UserProfile) o; - return super.equals(other) && Objects.equals(this.username, other.username) - && Objects.equals(this.avatar, other.avatar); + public static UserProfile getUser(long id) { + final var user = CACHE.computeIfAbsent(id, UserProfile::getNewUser); + if (NONE.equals(user)) { + CACHE.remove(id); } - return false; + return user; } - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), this.username, this.avatar); + private static UserProfile getNewUser(long id) { + try { + final var data = OAuthUtils.getWithHeader(MapillaryConfig.getUrls().getUserInformation(id)); + return JsonUserProfileDecoder.decodeUserProfile(data); + } catch (IOException exception) { + Logging.error(exception); + } + return NONE; } } diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/spi/preferences/IMapillaryUrls.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/spi/preferences/IMapillaryUrls.java index c0b37f06e..d7b16a052 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/spi/preferences/IMapillaryUrls.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/spi/preferences/IMapillaryUrls.java @@ -205,14 +205,15 @@ static MapillaryImageUtils.ImageProperties[] getDefaultImageInformation() { MapillaryImageUtils.ImageProperties.COMPUTED_ALTITUDE, MapillaryImageUtils.ImageProperties.COMPUTED_COMPASS_ANGLE, MapillaryImageUtils.ImageProperties.COMPUTED_GEOMETRY, - MapillaryImageUtils.ImageProperties.COMPUTED_ROTATION, MapillaryImageUtils.ImageProperties.EXIF_ORIENTATION, - MapillaryImageUtils.ImageProperties.GEOMETRY, MapillaryImageUtils.ImageProperties.HEIGHT, - MapillaryImageUtils.ImageProperties.ID, MapillaryImageUtils.ImageProperties.QUALITY_SCORE, - MapillaryImageUtils.ImageProperties.SEQUENCE, MapillaryImageUtils.ImageProperties.THUMB_1024_URL, - MapillaryImageUtils.ImageProperties.THUMB_2048_URL, MapillaryImageUtils.ImageProperties.THUMB_256_URL, - MapillaryImageUtils.ImageProperties.THUMB_ORIGINAL_URL, MapillaryImageUtils.ImageProperties.WIDTH, - MapillaryImageUtils.ImageProperties.WORST_IMAGE).distinct().sorted() - .toArray(MapillaryImageUtils.ImageProperties[]::new); + MapillaryImageUtils.ImageProperties.COMPUTED_ROTATION, MapillaryImageUtils.ImageProperties.CREATOR, + MapillaryImageUtils.ImageProperties.EXIF_ORIENTATION, MapillaryImageUtils.ImageProperties.GEOMETRY, + MapillaryImageUtils.ImageProperties.HEIGHT, MapillaryImageUtils.ImageProperties.ID, + MapillaryImageUtils.ImageProperties.MAKE, MapillaryImageUtils.ImageProperties.MODEL, + MapillaryImageUtils.ImageProperties.QUALITY_SCORE, MapillaryImageUtils.ImageProperties.SEQUENCE, + MapillaryImageUtils.ImageProperties.THUMB_1024_URL, MapillaryImageUtils.ImageProperties.THUMB_2048_URL, + MapillaryImageUtils.ImageProperties.THUMB_256_URL, MapillaryImageUtils.ImageProperties.THUMB_ORIGINAL_URL, + MapillaryImageUtils.ImageProperties.WIDTH, MapillaryImageUtils.ImageProperties.WORST_IMAGE).distinct() + .sorted().toArray(MapillaryImageUtils.ImageProperties[]::new); } /** @@ -253,6 +254,17 @@ default String getOrganizationInformation(long id) { + queryString(Collections.singletonMap(FIELDS, "slug,name,description")); } + /** + * Get user information for the specified user + * + * @return The URL to get user information + */ + default URI getUserInformation(long id) { + checkIds(id); + return string2URL(MapillaryConfig.getUrls().getBaseMetaDataUrl() + id + + queryString(Collections.singletonMap(FIELDS, "username,id"))); + } + /** * Get user information for the current user * diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryImageUtils.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryImageUtils.java index e9b81a824..2ede37ae5 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryImageUtils.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryImageUtils.java @@ -7,7 +7,6 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; -import java.util.regex.Matcher; import java.util.regex.Pattern; import jakarta.annotation.Nonnull; @@ -18,11 +17,13 @@ import org.openstreetmap.josm.data.osm.INode; import org.openstreetmap.josm.data.osm.IPrimitive; import org.openstreetmap.josm.data.osm.IWay; +import org.openstreetmap.josm.data.osm.Tagged; import org.openstreetmap.josm.plugins.mapillary.cache.CacheUtils; import org.openstreetmap.josm.plugins.mapillary.cache.Caches; import org.openstreetmap.josm.plugins.mapillary.cache.MapillaryCache; import org.openstreetmap.josm.plugins.mapillary.data.mapillary.OrganizationRecord; import org.openstreetmap.josm.plugins.mapillary.gui.workers.MapillaryNodeDownloader; +import org.openstreetmap.josm.plugins.mapillary.model.UserProfile; import org.openstreetmap.josm.tools.Logging; import org.openstreetmap.josm.tools.UncheckedParseException; import org.openstreetmap.josm.tools.date.DateUtils; @@ -77,7 +78,7 @@ public static IWay getSequence(@Nullable N image) { public static Instant getDate(@Nonnull INode img) { if (Instant.EPOCH.equals(img.getInstant()) && !Instant.EPOCH.equals(getCapturedAt(img))) { try { - Instant instant = getCapturedAt(img); + final var instant = getCapturedAt(img); img.setInstant(instant); return instant; } catch (NumberFormatException e) { @@ -172,7 +173,7 @@ public static CompletableFuture getImage(@Nonnull INode */ public static boolean isDownloadable(@Nullable INode node) { if (node != null) { - Matcher matcher = BASE_IMAGE_KEY.matcher(""); + final var matcher = BASE_IMAGE_KEY.matcher(""); for (String key : node.getKeys().keySet()) { matcher.reset(key); if (matcher.matches()) { @@ -192,9 +193,9 @@ public static boolean isDownloadable(@Nullable INode node) { */ private static void cacheImageFuture(INode image, CompletableFuture completableFuture, CacheEntry entry) { - if (entry instanceof BufferedImageCacheEntry) { + if (entry instanceof BufferedImageCacheEntry bufferedImageCacheEntry) { // Using the BufferedImageCacheEntry may speed up processing, if the image is already loaded. - completableFuture.complete((BufferedImageCacheEntry) entry); + completableFuture.complete(bufferedImageCacheEntry); } else if (entry != null && entry.getContent() != null) { // Fall back. More expensive if the image has already been loaded twice. completableFuture.complete(new BufferedImageCacheEntry(entry.getContent())); @@ -213,8 +214,8 @@ private static void cacheImageFuture(INode image, CompletableFuture 0) { image.setOsmId(id, 1); } @@ -302,7 +303,7 @@ public static boolean isImage(@Nullable IPrimitive node) { @Nonnull public static OrganizationRecord getOrganization(@Nullable INode img) { if (img != null) { - final String organizationKey = ImageProperties.ORGANIZATION_ID.toString(); + final var organizationKey = ImageProperties.ORGANIZATION_ID.toString(); if (img.hasKey(organizationKey)) { return OrganizationRecord.getOrganization(img.get(organizationKey)); } @@ -314,6 +315,21 @@ public static OrganizationRecord getOrganization(@Nullable INode img) { return OrganizationRecord.NULL_RECORD; } + /** + * Get the user for the image + * + * @param mapillaryImage The image to get the user profile for + * @return The user profile + */ + @Nonnull + public static UserProfile getUser(@Nullable Tagged mapillaryImage) { + if (mapillaryImage != null && mapillaryImage.hasKey(MapillaryImageUtils.ImageProperties.CREATOR.toString())) { + final var creator = mapillaryImage.get(MapillaryImageUtils.ImageProperties.CREATOR.toString()); + return UserProfile.getUser(creator); + } + return UserProfile.NONE; + } + private MapillaryImageUtils() { /* No op */ } @@ -385,6 +401,8 @@ public enum ImageProperties { * @see #EXIF_ORIENTATION */ COMPUTED_ROTATION, + /** The username and user id who uploaded the image ({@code {username: string, id: int}}) */ + CREATOR, /** * Original orientation of the image * @@ -401,6 +419,10 @@ public enum ImageProperties { HEIGHT, /** 1 if the image is panoramic */ IS_PANO, + /** The manufacturer of the camera */ + MAKE, + /** The model of the camera */ + MODEL, /** The id of the organization */ ORGANIZATION_ID, /** A 256px image (max width). You should prefer {@link #WORST_IMAGE}. */ diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonImageDetailsDecoder.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonImageDetailsDecoder.java index 0529d0ef0..c74e591d5 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonImageDetailsDecoder.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonImageDetailsDecoder.java @@ -102,7 +102,7 @@ private static Pair decodeImageInfo(@Nullable final JsonO // Clean up bad key value combinations // Using for loop to (hopefully) fix JOSM #21070 and #21072 for (Tag tag : map.getTags()) { - // Tag#getKey and Tag#getValue are never null. According to docs. + // Tag#key and Tag#getValue are never null. According to docs. if (Utils.isStripEmpty(tag.getKey()) || Utils.isStripEmpty(tag.getValue())) { image.put(tag.getKey(), null); } diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonUserProfileDecoder.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonUserProfileDecoder.java index 318efaf05..dfe4fb6c8 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonUserProfileDecoder.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonUserProfileDecoder.java @@ -8,17 +8,19 @@ import javax.imageio.ImageIO; import javax.swing.ImageIcon; -import jakarta.json.JsonObject; import org.openstreetmap.josm.plugins.mapillary.model.UserProfile; import org.openstreetmap.josm.plugins.mapillary.spi.preferences.IMapillaryUrls; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Logging; +import jakarta.json.JsonObject; + /** * Decodes the JSON returned by {@link IMapillaryUrls} into Java objects. * Takes a {@link JsonObject} and {@link #decodeUserProfile(JsonObject)} tries to convert it to a {@link UserProfile}. */ public final class JsonUserProfileDecoder { + /** The avatar for profiles without an avatar */ private static final ImageIcon FAKE_AVATAR = new ImageProvider("fake-avatar").get(); private JsonUserProfileDecoder() { @@ -35,13 +37,13 @@ public static UserProfile decodeUserProfile(JsonObject json) { if (json == null) { return null; } - String username = json.getString("username", null); - String key = json.getString("key", null); + final var username = json.getString("username", null); + final var key = json.getString("id", null); if (key == null || username == null) { return null; } - String avatar = json.getString("avatar", null); + final var avatar = json.getString("avatar", null); ImageIcon icon = null; if (avatar != null) { try { @@ -56,6 +58,6 @@ public static UserProfile decodeUserProfile(JsonObject json) { if (icon == null) { icon = FAKE_AVATAR; } - return new UserProfile(key, username, icon); + return new UserProfile(Long.parseLong(key), username, icon); } } diff --git a/test/data/__files/api/v4/responses/graph/104214208486349.json b/test/data/__files/api/v4/responses/graph/104214208486349.json new file mode 100644 index 000000000..bcb968c32 --- /dev/null +++ b/test/data/__files/api/v4/responses/graph/104214208486349.json @@ -0,0 +1 @@ +{"username":"vorpalblade","id":"104214208486349"} diff --git a/test/data/__files/api/v4/responses/graph/104214208486350.json b/test/data/__files/api/v4/responses/graph/104214208486350.json new file mode 100644 index 000000000..b8f43d376 --- /dev/null +++ b/test/data/__files/api/v4/responses/graph/104214208486350.json @@ -0,0 +1 @@ +{"username":"mapillary_userÄ2!","id":"104214208486349", "comment": "Not an actual user! Don't update!"} diff --git a/test/unit/org/openstreetmap/josm/plugins/mapillary/model/ImageDetectionTest.java b/test/unit/org/openstreetmap/josm/plugins/mapillary/model/ImageDetectionTest.java index 093010c8e..56f7e3ffc 100644 --- a/test/unit/org/openstreetmap/josm/plugins/mapillary/model/ImageDetectionTest.java +++ b/test/unit/org/openstreetmap/josm/plugins/mapillary/model/ImageDetectionTest.java @@ -29,8 +29,8 @@ void testBasics() { assertEquals(1, id.getImageKey()); assertEquals(3, trafficsign.getImageKey()); - assertEquals(2, id.getKey()); - assertEquals(4, trafficsign.getKey()); + assertEquals(2, id.key()); + assertEquals(4, trafficsign.key()); assertFalse(id.isTrafficSign()); assertTrue(trafficsign.isTrafficSign()); diff --git a/test/unit/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonUserProfileDecoderTest.java b/test/unit/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonUserProfileDecoderTest.java index 3c8b33ed8..c1180cd03 100644 --- a/test/unit/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonUserProfileDecoderTest.java +++ b/test/unit/org/openstreetmap/josm/plugins/mapillary/utils/api/JsonUserProfileDecoderTest.java @@ -3,7 +3,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; @@ -15,14 +14,15 @@ import java.nio.file.Files; import java.nio.file.Paths; -import jakarta.json.Json; -import jakarta.json.JsonReader; import org.junit.jupiter.api.Test; import org.openstreetmap.josm.plugins.mapillary.model.UserProfile; import org.openstreetmap.josm.plugins.mapillary.utils.JsonUtil; import org.openstreetmap.josm.plugins.mapillary.utils.TestUtil; import org.openstreetmap.josm.testutils.annotations.BasicPreferences; +import jakarta.json.Json; +import jakarta.json.JsonReader; + @BasicPreferences class JsonUserProfileDecoderTest { private static Object getFakeAvatar() { @@ -37,33 +37,29 @@ void testUtilityClass() { private static InputStream getJsonInputStream(final String path) throws IOException, URISyntaxException { String fileContent = String.join("\n", Files.readAllLines( Paths.get(JsonUserProfileDecoderTest.class.getResource(path).toURI()), StandardCharsets.UTF_8)); - fileContent = fileContent.replace("https://d4vkkeqw582u.cloudfront.net/3f9f044b34b498ddfb9afbb6/profile.png", - JsonUserProfileDecoder.class.getResource("/images/fake-avatar.png").toString()); - fileContent = fileContent.replace("https://example.org", - JsonUserProfileDecoder.class.getResource("/api/v3/responses/userProfile.json").toString()); return new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); } @Test void testDecodeUserProfile() throws IOException, URISyntaxException, IllegalArgumentException { - try (InputStream inputStream = getJsonInputStream("/api/v3/responses/userProfile.json"); + try (InputStream inputStream = getJsonInputStream("/__files/api/v4/responses/graph/104214208486349.json"); JsonReader reader = Json.createReader(inputStream)) { UserProfile profile = JsonUserProfileDecoder.decodeUserProfile(reader.readObject()); - assertEquals("2BJl04nvnfW1y2GNaj7x5w", profile.getKey()); - assertEquals("gyllen", profile.getUsername()); - assertNotNull(profile.getAvatar()); - assertNotSame(getFakeAvatar(), profile.getAvatar()); + assertEquals(104_214_208_486_349L, profile.key()); + assertEquals("vorpalblade", profile.username()); + assertSame(getFakeAvatar(), profile.avatar(), "avatar not yet in response"); } } @Test void testDecodeUserProfile2() throws IOException, URISyntaxException, IllegalArgumentException { - try (InputStream inputStream = getJsonInputStream("/api/v3/responses/userProfile2.json"); + try (InputStream inputStream = getJsonInputStream("/__files/api/v4/responses/graph/104214208486350.json"); JsonReader reader = Json.createReader(inputStream)) { UserProfile profile = JsonUserProfileDecoder.decodeUserProfile(reader.readObject()); - assertEquals("abcdefg1", profile.getKey()); - assertEquals("mapillary_userÄ2!", profile.getUsername()); - assertSame(getFakeAvatar(), profile.getAvatar()); + assertEquals(104_214_208_486_349L, profile.key(), + "This should be the same as 104214208486349.json except with a different username"); + assertEquals("mapillary_userÄ2!", profile.username()); + assertSame(getFakeAvatar(), profile.avatar(), "avatar not yet in response"); } } @@ -74,13 +70,13 @@ void testDecodeInvalidUserProfile() throws IllegalArgumentException, SecurityExc assertNull(JsonUserProfileDecoder.decodeUserProfile(JsonUtil.string2jsonObject("{\"key\":\"arbitrary_key\"}"))); UserProfile profile = JsonUserProfileDecoder.decodeUserProfile( - JsonUtil.string2jsonObject("{\"key\":\"arbitrary_key\", \"username\":\"arbitrary_username\"}")); + JsonUtil.string2jsonObject("{\"id\":\"-1\", \"username\":\"arbitrary_username\"}")); assertNotNull(profile); - assertSame(getFakeAvatar(), profile.getAvatar()); + assertSame(getFakeAvatar(), profile.avatar()); profile = JsonUserProfileDecoder.decodeUserProfile(JsonUtil.string2jsonObject( - "{\"key\":\"arbitrary_key\", \"username\":\"arbitrary_username\", \"avatar\":\"https://127.0.0.1/nonExistingAvatarFile\"}")); + "{\"id\":\"-1\", \"username\":\"arbitrary_username\", \"avatar\":\"https://127.0.0.1/nonExistingAvatarFile\"}")); assertNotNull(profile); - assertSame(getFakeAvatar(), profile.getAvatar()); + assertSame(getFakeAvatar(), profile.avatar()); } }