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());
}
}