From cc2193b0f74b673b7cb4d6edc55afc98c41dd9ed Mon Sep 17 00:00:00 2001 From: Taylor Smock Date: Thu, 18 Apr 2024 12:45:56 -0600 Subject: [PATCH] Allow filtering by users (since Mapillary added the data back to the vector tiles) Signed-off-by: Taylor Smock --- .../josm/plugins/mapillary/cache/Caches.java | 29 +++-- .../data/mapillary/MapillaryDownloader.java | 8 +- .../data/mapillary/OrganizationRecord.java | 13 +- .../gui/dialog/MapillaryFilterDialog.java | 115 ++++++++++++++---- .../dialog/UserProfileListCellRenderer.java | 31 +++++ .../mapillary/gui/layer/MapillaryLayer.java | 2 + .../mapillary/model/ImageDetection.java | 4 +- .../plugins/mapillary/model/UserProfile.java | 87 +++++++++++-- .../spi/preferences/IMapillaryUrls.java | 32 ++--- .../mapillary/utils/MapillaryImageUtils.java | 13 +- .../utils/MapillaryMapFeatureUtils.java | 4 +- 11 files changed, 271 insertions(+), 67 deletions(-) create mode 100644 src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/UserProfileListCellRenderer.java diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java index 042e06247..d1d0da4b1 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java @@ -8,6 +8,7 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -17,8 +18,8 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; import java.util.stream.Stream; import javax.swing.JOptionPane; @@ -38,6 +39,8 @@ import org.openstreetmap.josm.gui.Notification; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.plugins.mapillary.model.UserProfile; +import org.openstreetmap.josm.plugins.mapillary.oauth.OAuthUtils; +import org.openstreetmap.josm.plugins.mapillary.utils.api.JsonUserProfileDecoder; import org.openstreetmap.josm.spi.preferences.Config; import org.openstreetmap.josm.tools.JosmRuntimeException; import org.openstreetmap.josm.tools.Logging; @@ -103,6 +106,16 @@ public final class Caches { final IElementAttributes fullImageElementCacheAttributes = FULL_IMAGE_CACHE.getDefaultElementAttributes(); fullImageElementCacheAttributes.setMaxLife(maxTime); FULL_IMAGE_CACHE.setDefaultElementAttributes(fullImageElementCacheAttributes); + + USER_PROFILE_CACHE.setDefaultSupplier(url -> { + try { + final var data = OAuthUtils.getWithHeader(URI.create(url)); + return JsonUserProfileDecoder.decodeUserProfile(data); + } catch (IOException e) { + Logging.error(e); + return null; + } + }); } public static File getCacheDirectory() { @@ -131,7 +144,7 @@ public static class MapillaryCacheAccess { @Nonnull private final List> validators; @Nullable - private Supplier defaultSupplier; + private Function defaultSupplier; private boolean rateLimited; @@ -150,7 +163,7 @@ public MapillaryCacheAccess(@Nonnull CacheAccess cacheAccess, @Nullab * * @param defaultSupplier The default supplier */ - public void setDefaultSupplier(@Nullable Supplier defaultSupplier) { + public void setDefaultSupplier(@Nullable Function defaultSupplier) { this.defaultSupplier = defaultSupplier; } @@ -185,7 +198,7 @@ public V get(@Nonnull String url) { if (this.defaultSupplier != null) { return this.get(url, this.defaultSupplier); } else { - return this.get(url, () -> null); + return this.get(url, (u) -> null); } } @@ -202,7 +215,7 @@ public Future get(@Nonnull String url, @Nonnull ForkJoinPool pool) { if (this.defaultSupplier != null) { return this.get(url, pool, this.defaultSupplier); } else { - return this.get(url, pool, () -> null); + return this.get(url, pool, (u) -> null); } } @@ -214,7 +227,7 @@ public Future get(@Nonnull String url, @Nonnull ForkJoinPool pool) { * @return The type the supplier returns */ @Nullable - public V get(@Nonnull String url, @Nonnull Supplier supplier) { + public V get(@Nonnull String url, @Nonnull Function supplier) { if (this.cacheAccess.get(url) != null) { return this.cacheAccess.get(url); } @@ -228,7 +241,7 @@ public V get(@Nonnull String url, @Nonnull Supplier supplier) { } final V newReturnObject; synchronized (this) { - newReturnObject = this.cacheAccess.get(url) == null ? supplier.get() : this.cacheAccess.get(url); + newReturnObject = this.cacheAccess.get(url) == null ? supplier.apply(url) : this.cacheAccess.get(url); if (newReturnObject != null && this.validators.stream().allMatch(p -> p.test(newReturnObject))) { this.cacheAccess.put(url, newReturnObject); } else if (newReturnObject != null) { @@ -253,7 +266,7 @@ public V get(@Nonnull String url, @Nonnull Supplier supplier) { * @param supplier The supplier to get the object with * @return A future with the object, when it completes. */ - public Future get(@Nonnull String url, @Nonnull ForkJoinPool pool, @Nonnull Supplier supplier) { + public Future get(@Nonnull String url, @Nonnull ForkJoinPool pool, @Nonnull Function supplier) { if (this.cacheAccess.get(url) != null) { return CompletableFuture.completedFuture(this.cacheAccess.get(url)); } diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/MapillaryDownloader.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/MapillaryDownloader.java index 18ee43c93..d57946210 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/MapillaryDownloader.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/MapillaryDownloader.java @@ -57,8 +57,8 @@ public static Map> downloadImages(final long... imag } final Caches.MapillaryCacheAccess metaDataCache = Caches.META_DATA_CACHE; String url = MapillaryConfig.getUrls().getImageInformation(images); - String stringJson = metaDataCache.get(url, () -> { - final JsonObject jsonObject = getUrlResponse(url); + String stringJson = metaDataCache.get(url, (u) -> { + final JsonObject jsonObject = getUrlResponse(u); return jsonObject != null ? jsonObject.toString() : null; }); if (stringJson == null) { @@ -118,8 +118,8 @@ public static long[] downloadSequence(@Nonnull String sequence) { } } final String url = MapillaryConfig.getUrls().getImagesBySequences(sequence); - final String response = Caches.META_DATA_CACHE.get(url, () -> { - final JsonObject urlResponse = getUrlResponse(url); + final String response = Caches.META_DATA_CACHE.get(url, (u) -> { + final JsonObject urlResponse = getUrlResponse(u); return urlResponse == null ? null : urlResponse.toString(); }); if (response != null) { 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 9797b76d9..1671a62ea 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 @@ -2,9 +2,11 @@ package org.openstreetmap.josm.plugins.mapillary.data.mapillary; import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.Serializable; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; import java.util.Map; @@ -16,6 +18,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import jakarta.json.Json; import jakarta.json.JsonObject; import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile; import org.openstreetmap.josm.data.osm.INode; @@ -63,7 +66,7 @@ public record OrganizationRecord(long id, String name, String niceName, String d @Nonnull private static ImageIcon createAvatarIcon(@Nullable String avatar) { if (avatar != null && !avatar.isEmpty()) { - final BufferedImage avatarImage = Caches.META_IMAGES.get(avatar, () -> fetchAvatarIcon(avatar)); + final BufferedImage avatarImage = Caches.META_IMAGES.get(avatar, OrganizationRecord::fetchAvatarIcon); return avatarImage != null ? new ImageIcon(avatarImage) : ImageProvider.createBlankIcon(ImageSizes.DEFAULT); } return ImageProvider.getEmpty(ImageSizes.DEFAULT); @@ -106,7 +109,13 @@ public static OrganizationRecord getOrganization(long id) { * @return The organization */ public static OrganizationRecord getOrganization(String id) { - return getOrganization(Long.parseLong(id)); + if (id.matches("^\\d+$")) { + return getOrganization(Long.parseLong(id)); + } else { // Assume json + try (var reader = Json.createReader(new ByteArrayInputStream(id.getBytes(StandardCharsets.UTF_8)))) { + return getOrganization(Long.parseLong(reader.readObject().getString("id"))); + } + } } /** 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 173b51117..83355bdf7 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 @@ -18,8 +18,10 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -40,6 +42,7 @@ import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile; import org.openstreetmap.josm.data.osm.INode; import org.openstreetmap.josm.data.osm.IPrimitive; +import org.openstreetmap.josm.data.osm.Tagged; import org.openstreetmap.josm.data.vector.VectorNode; import org.openstreetmap.josm.gui.MainApplication; import org.openstreetmap.josm.gui.SideButton; @@ -48,6 +51,7 @@ import org.openstreetmap.josm.gui.layer.MainLayerManager; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.gui.util.GuiHelper; +import org.openstreetmap.josm.gui.widgets.JosmComboBox; import org.openstreetmap.josm.plugins.datepicker.IDatePicker; import org.openstreetmap.josm.plugins.mapillary.data.mapillary.OrganizationRecord; import org.openstreetmap.josm.plugins.mapillary.data.mapillary.OrganizationRecord.OrganizationRecordListener; @@ -55,6 +59,7 @@ import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer; import org.openstreetmap.josm.plugins.mapillary.gui.layer.PointObjectLayer; import org.openstreetmap.josm.plugins.mapillary.gui.widget.DisableShortcutsOnFocusGainedJSpinner; +import org.openstreetmap.josm.plugins.mapillary.model.UserProfile; import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryImageUtils; import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties; import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils; @@ -72,7 +77,7 @@ * @author nokutu */ public final class MapillaryFilterDialog extends ToggleDialog - implements OrganizationRecordListener, MVTTile.TileListener { + implements OrganizationRecordListener, UserProfile.UserProfileListener, MVTTile.TileListener { @Serial private static final long serialVersionUID = -4192029663670922103L; @@ -82,8 +87,7 @@ public final class MapillaryFilterDialog extends ToggleDialog private static final String[] TIME_LIST = { tr("Years"), tr("Months"), tr("Days") }; private static final long[] TIME_FACTOR = new long[] { 31_536_000_000L, // = 365 * 24 * 60 * 60 * 1000 = number of - // ms - // in a year + // ms in a year 2_592_000_000L, // = 30 * 24 * 60 * 60 * 1000 = number of ms in a month 86_400_000 // = 24 * 60 * 60 * 1000 = number of ms in a day }; @@ -93,7 +97,10 @@ public final class MapillaryFilterDialog extends ToggleDialog private final transient ListenerList destroyable = ListenerList.create(); private final JLabel organizationLabel = new JLabel(tr("Org")); - final JComboBox organizations = new JComboBox<>(); + final JosmComboBox organizations = new JosmComboBox<>(); + + private final JLabel userLabel = new JLabel(tr("User")); + final JosmComboBox users = new JosmComboBox<>(); private boolean destroyed; @@ -150,27 +157,40 @@ private void addUserGroupFilters(JPanel panel) { final var userSearchPanel = new JPanel(); userSearchPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); - organizationLabel.setToolTipText(tr("Organizations")); - userSearchPanel.add(organizationLabel); + this.userLabel.setToolTipText(tr("Mapillary Users")); + userSearchPanel.add(this.userLabel); + userSearchPanel.add(this.users, GBC.eol()); + this.users.addItem(UserProfile.NONE); + this.organizationLabel.setToolTipText(tr("Organizations")); + userSearchPanel.add(this.organizationLabel); userSearchPanel.add(this.organizations); - organizations.addItem(OrganizationRecord.NULL_RECORD); - for (Component comp : Arrays.asList(organizationLabel, organizations)) { + this.organizations.addItem(OrganizationRecord.NULL_RECORD); + for (Component comp : Arrays.asList(this.userLabel, this.users, this.organizationLabel, this.organizations)) { comp.setEnabled(false); } - organizations.setRenderer(new OrganizationListCellRenderer()); + this.users.setRenderer(new UserProfileListCellRenderer()); + this.organizations.setRenderer(new OrganizationListCellRenderer()); panel.add(userSearchPanel, GBC.eol().anchor(GridBagConstraints.LINE_START)); OrganizationRecord.addOrganizationListener(this); OrganizationRecord.getOrganizations().forEach(this::organizationAdded); + UserProfile.addUserProfileListener(this); + UserProfile.getUsers().forEach(this::userProfileAdded); this.organizations .addItemListener(l -> this.shouldHidePredicate.organization = (OrganizationRecord) l.getItem()); + this.users.addItemListener(l -> this.shouldHidePredicate.userProfile = (UserProfile) l.getItem()); this.resetObjects.addListener(() -> organizations.setSelectedItem(OrganizationRecord.NULL_RECORD)); ResetListener setListener = () -> this.shouldHidePredicate.organization = (OrganizationRecord) this.organizations .getSelectedItem(); this.resetObjects.addListener(setListener); setListener.reset(); + this.resetObjects.addListener(() -> this.users.setSelectedItem(UserProfile.NONE)); + ResetListener userResetListener = () -> this.shouldHidePredicate.userProfile = (UserProfile) this.users + .getSelectedItem(); + userResetListener.reset(); + this.resetObjects.addListener(userResetListener); } /** @@ -432,6 +452,7 @@ private static class ImageFilterPredicate implements Predicate { Instant endDateRefresh; Instant startDateRefresh; OrganizationRecord organization; + UserProfile userProfile; boolean smartAdd; public ImageFilterPredicate() { @@ -480,11 +501,12 @@ public boolean test(INode img, Collection currentSelection) { || (this.imageTypes == ImageTypes.NON_PANORAMIC && MapillaryImageUtils.IS_PANORAMIC.test(img))) { return true; } - if (MapillaryImageUtils.getKey(img) > 0) { + if (MapillaryImageUtils.getKey(img) > 0 && MapillaryImageUtils.getSequenceKey(img) != null) { // Filter on organizations - return !OrganizationRecord.NULL_RECORD.equals(this.organization) - && MapillaryImageUtils.getSequenceKey(img) != null - && this.organization.id() != MapillaryImageUtils.getOrganization(img).id(); + if (checkOrganization(img)) { + return true; + } + return checkUser(img); } return false; } @@ -496,6 +518,28 @@ final void updateLayerVisible() { this.layerVisible = MapillaryLayer.hasInstance() && MapillaryLayer.getInstance().isVisible(); } + /** + * Check if the organization does not match + * + * @param img The image to check + * @return {@code true} if the organization does not match + */ + private boolean checkOrganization(INode img) { + return this.organization != null && !OrganizationRecord.NULL_RECORD.equals(this.organization) + && this.organization.id() != MapillaryImageUtils.getOrganization(img).id(); + } + + /** + * Check if the user does not match + * + * @param img The image to check + * @return {@code true} if the user does not match + */ + private boolean checkUser(Tagged img) { + return this.userProfile != null && !UserProfile.NONE.equals(this.userProfile) + && this.userProfile.key() != MapillaryImageUtils.getUser(img).key(); + } + /** * @param img The image to check * @return {@code true} if the start date is after the image date @@ -573,6 +617,7 @@ public void destroy() { MainApplication.getMap().removeToggleDialog(this); } OrganizationRecord.removeOrganizationListener(this); + UserProfile.removeUserProfileListener(this); destroyed = true; } destroyInstance(); @@ -580,14 +625,7 @@ public void destroy() { @Override public void organizationAdded(OrganizationRecord organization) { - var add = true; - for (var i = 0; i < organizations.getItemCount(); i++) { - if (organizations.getItemAt(i).equals(organization)) { - add = false; - break; - } - } - if (add) { + if (organization != null && comboBoxDoesNotContainItem(organizations, organization)) { GuiHelper.runInEDT(() -> organizations.addItem(organization)); } for (Component comp : Arrays.asList(organizationLabel, organizations)) { @@ -596,6 +634,41 @@ public void organizationAdded(OrganizationRecord organization) { } } + @Override + public void userProfileAdded(UserProfile userProfile) { + if (userProfile != null && comboBoxDoesNotContainItem(users, userProfile)) { + GuiHelper.runInEDT(() -> { + final var list = new ArrayList<>(users.getModel().asCollection()); + list.add(userProfile); + list.sort(Comparator.comparing(UserProfile::username)); + final var model = users.getModel(); + model.removeAllElements(); + model.addAllElements(list); + }); + } + for (Component comp : Arrays.asList(userLabel, users)) { + GuiHelper + .runInEDT(() -> comp.setEnabled(users.getItemCount() > 1 || !UserProfile.NONE.equals(userProfile))); + } + } + + /** + * Check if a combo box does not contain a specified item + * + * @param comboBox The combo box to look through + * @param object The object to look for + * @return {@code true} if the combobox did not contain the object + * @param The object type + */ + private static boolean comboBoxDoesNotContainItem(JComboBox comboBox, T object) { + for (var i = 0; i < comboBox.getItemCount(); i++) { + if (comboBox.getItemAt(i).equals(object)) { + return false; + } + } + return true; + } + /** * Image types (pano, non-pano, and all) */ diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/UserProfileListCellRenderer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/UserProfileListCellRenderer.java new file mode 100644 index 000000000..090af8358 --- /dev/null +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/UserProfileListCellRenderer.java @@ -0,0 +1,31 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.mapillary.gui.dialog; + +import java.awt.Component; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.JLabel; +import javax.swing.JList; + +import org.openstreetmap.josm.plugins.mapillary.model.UserProfile; + +/** + * A renderer for {@link UserProfile} objects + */ +class UserProfileListCellRenderer extends DefaultListCellRenderer { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) { + JLabel comp = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value instanceof UserProfile userProfile) { + if (userProfile.username() != null && !userProfile.username().isBlank()) { + comp.setText(userProfile.username()); + } else if (!UserProfile.NONE.equals(userProfile)) { + comp.setText(Long.toString(userProfile.key())); + } else { + comp.setText(""); + } + } + return comp; + } +} diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java index 8714c44fe..db3bb293a 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java @@ -89,6 +89,7 @@ import org.openstreetmap.josm.plugins.mapillary.gui.layer.geoimage.MapillaryImageEntry; import org.openstreetmap.josm.plugins.mapillary.gui.workers.MapillaryNodeDownloader; import org.openstreetmap.josm.plugins.mapillary.gui.workers.MapillarySequenceDownloader; +import org.openstreetmap.josm.plugins.mapillary.model.UserProfile; import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryColorScheme; import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryImageUtils; import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryKeys; @@ -176,6 +177,7 @@ private MapillaryLayer() { SwingUtilities.invokeLater(OldVersionDialog::showOldVersion); this.addTileDownloadListener(OrganizationRecord::addFromTile); + this.addTileDownloadListener(UserProfile::addFromTile); this.addTileDownloadListener(MapillaryFilterDialog.getInstance()); this.getData().addSelectionListener(this); this.getData().addHighlightUpdateListener(this); 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 7335002c0..e88a7b032 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 @@ -337,9 +337,9 @@ private static List> getDetections(long key) { return Collections.emptyList(); } final String urlString = MapillaryConfig.getUrls().getDetectionInformation(key); - final String jsonString = Caches.META_DATA_CACHE.get(urlString, () -> { + final String jsonString = Caches.META_DATA_CACHE.get(urlString, (u) -> { try { - final JsonObject jsonObject = OAuthUtils.getWithHeader(URI.create(urlString)); + final JsonObject jsonObject = OAuthUtils.getWithHeader(URI.create(u)); return jsonObject != null ? jsonObject.toString() : null; } catch (IOException e) { Logging.error(e); 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 6bfc59dac..ce945dd69 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,19 +1,25 @@ // License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.mapillary.model; -import java.io.IOException; +import static org.openstreetmap.josm.plugins.mapillary.cache.Caches.USER_PROFILE_CACHE; + +import java.io.Serializable; import java.io.StringReader; +import java.util.Collection; +import java.util.Collections; 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.data.imagery.vectortile.mapbox.MVTTile; +import org.openstreetmap.josm.data.osm.INode; import org.openstreetmap.josm.plugins.mapillary.spi.preferences.MapillaryConfig; +import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryImageUtils; import org.openstreetmap.josm.plugins.mapillary.utils.api.JsonUserProfileDecoder; import org.openstreetmap.josm.tools.ImageProvider; -import org.openstreetmap.josm.tools.Logging; +import org.openstreetmap.josm.tools.ListenerList; /** * A profile for a user @@ -22,9 +28,10 @@ * @param username The username for the user * @param avatar The avatar for the user */ -public record UserProfile(long key, String username, ImageIcon avatar) { +public record UserProfile(long key, String username, ImageIcon avatar) implements Serializable { private static final Map CACHE = new ConcurrentHashMap<>(1); + private static final ListenerList LISTENERS = ListenerList.create(); /** A default user profile */ public static final UserProfile NONE = new UserProfile(Long.MIN_VALUE, "", ImageProvider.createBlankIcon(ImageProvider.ImageSizes.DEFAULT)); @@ -33,6 +40,12 @@ public record UserProfile(long key, String username, ImageIcon avatar) { CACHE.put(NONE.key(), NONE); } + /** + * Get a user from a json string + * + * @param json The json string + * @return The user profile + */ public static UserProfile getUser(String json) { final UserProfile user; try (var reader = Json.createReader(new StringReader(json))) { @@ -41,6 +54,12 @@ public static UserProfile getUser(String json) { return CACHE.computeIfAbsent(user.key(), ignored -> user); } + /** + * Get the user given a user id + * + * @param id The user id + * @return The user profile + */ public static UserProfile getUser(long id) { final var user = CACHE.computeIfAbsent(id, UserProfile::getNewUser); if (NONE.equals(user)) { @@ -50,12 +69,60 @@ public static UserProfile getUser(long id) { } 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); + final var userProfile = USER_PROFILE_CACHE.get(MapillaryConfig.getUrls().getUserInformation(id).toString()); + if (userProfile == null) { + return NONE; } - return NONE; + LISTENERS.fireEvent(e -> e.userProfileAdded(userProfile)); + return userProfile; + } + + /** + * Get the currently downloaded and cached users + * + * @return The users + */ + public static Collection getUsers() { + return Collections.unmodifiableCollection(CACHE.values()); + } + + /** + * Read organizations from a tile and add them to the list + * + * @param tile The tile to read + */ + public static void addFromTile(MVTTile tile) { + tile.getData().getAllPrimitives().stream().filter(INode.class::isInstance).map(INode.class::cast) + .forEach(MapillaryImageUtils::getUser); + } + + /** + * Add a new user listener + * + * @param listener The listener to call + */ + public static void addUserProfileListener(UserProfileListener listener) { + LISTENERS.addListener(listener); + } + + /** + * Remove a user listener + * + * @param listener the listner to remove + */ + public static void removeUserProfileListener(UserProfileListener listener) { + LISTENERS.removeListener(listener); + } + + /** + * The interface for listening for new organizations + */ + public interface UserProfileListener extends Serializable { + /** + * Called when a new user is added + * + * @param userProfile The added user profile + */ + void userProfileAdded(UserProfile userProfile); } } 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 d7b16a052..300517b9e 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 @@ -199,21 +199,23 @@ default String getImageInformation(long image, @Nullable MapillaryImageUtils.Ima * @return The default image properties to get */ static MapillaryImageUtils.ImageProperties[] getDefaultImageInformation() { - return Stream.of(MapillaryImageUtils.ImageProperties.ALTITUDE, MapillaryImageUtils.ImageProperties.ATOMIC_SCALE, - MapillaryImageUtils.ImageProperties.BEST_IMAGE, MapillaryImageUtils.ImageProperties.CAPTURED_AT, - MapillaryImageUtils.ImageProperties.CAMERA_TYPE, MapillaryImageUtils.ImageProperties.COMPASS_ANGLE, - MapillaryImageUtils.ImageProperties.COMPUTED_ALTITUDE, - MapillaryImageUtils.ImageProperties.COMPUTED_COMPASS_ANGLE, - MapillaryImageUtils.ImageProperties.COMPUTED_GEOMETRY, - 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); + return Stream + .of(MapillaryImageUtils.ImageProperties.ALTITUDE, MapillaryImageUtils.ImageProperties.ATOMIC_SCALE, + MapillaryImageUtils.ImageProperties.BEST_IMAGE, MapillaryImageUtils.ImageProperties.CAPTURED_AT, + MapillaryImageUtils.ImageProperties.CAMERA_TYPE, MapillaryImageUtils.ImageProperties.COMPASS_ANGLE, + MapillaryImageUtils.ImageProperties.COMPUTED_ALTITUDE, + MapillaryImageUtils.ImageProperties.COMPUTED_COMPASS_ANGLE, + MapillaryImageUtils.ImageProperties.COMPUTED_GEOMETRY, + 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.ORGANIZATION, 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); } /** 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 2ede37ae5..6120ddc2b 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 @@ -306,6 +306,8 @@ public static OrganizationRecord getOrganization(@Nullable INode img) { final var organizationKey = ImageProperties.ORGANIZATION_ID.toString(); if (img.hasKey(organizationKey)) { return OrganizationRecord.getOrganization(img.get(organizationKey)); + } else if (img.hasKey(ImageProperties.ORGANIZATION.toString())) { + return OrganizationRecord.getOrganization(img.get(ImageProperties.ORGANIZATION.toString())); } IWay sequence = getSequence(img); if (sequence != null && sequence.hasKey(organizationKey)) { @@ -323,9 +325,10 @@ public static OrganizationRecord getOrganization(@Nullable INode img) { */ @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); + if (mapillaryImage != null && mapillaryImage.hasKey(ImageProperties.CREATOR_ID.toString())) { + return UserProfile.getUser(Long.parseLong(mapillaryImage.get(ImageProperties.CREATOR_ID.toString()))); + } else if (mapillaryImage != null && mapillaryImage.hasKey(ImageProperties.CREATOR.toString())) { + return UserProfile.getUser(mapillaryImage.get(ImageProperties.CREATOR.toString())); } return UserProfile.NONE; } @@ -403,6 +406,8 @@ public enum ImageProperties { COMPUTED_ROTATION, /** The username and user id who uploaded the image ({@code {username: string, id: int}}) */ CREATOR, + /** The id of the creator who uploaded the image */ + CREATOR_ID, /** * Original orientation of the image * @@ -423,6 +428,8 @@ public enum ImageProperties { MAKE, /** The model of the camera */ MODEL, + /** The organization field */ + ORGANIZATION, /** 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/MapillaryMapFeatureUtils.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryMapFeatureUtils.java index c382d9610..5141dd83b 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryMapFeatureUtils.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryMapFeatureUtils.java @@ -195,9 +195,9 @@ private static String getKeyValue(@Nullable final IPrimitive primitive, private static void updateMapFeature(@Nonnull final IPrimitive primitive) { final String url = MapillaryConfig.getUrls().getMapFeatureInformation(primitive.getId(), MapFeatureProperties.GEOMETRY, MapFeatureProperties.IMAGES, MapFeatureProperties.ALIGNED_DIRECTION); - final String json = Caches.META_DATA_CACHE.get(url, () -> { + final String json = Caches.META_DATA_CACHE.get(url, (u) -> { try { - return OAuthUtils.getWithHeader(URI.create(url)).toString(); + return OAuthUtils.getWithHeader(URI.create(u)).toString(); } catch (IOException e) { Logging.error(e); return null;