// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.dialogs.changeset;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GraphicsEnvironment;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.AbstractAction;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.downloadtasks.AbstractChangesetDownloadTask;
import org.openstreetmap.josm.actions.downloadtasks.ChangesetContentDownloadTask;
import org.openstreetmap.josm.actions.downloadtasks.ChangesetHeaderDownloadTask;
import org.openstreetmap.josm.actions.downloadtasks.ChangesetQueryTask;
import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
import org.openstreetmap.josm.data.osm.Changeset;
import org.openstreetmap.josm.data.osm.ChangesetCache;
import org.openstreetmap.josm.data.osm.ChangesetDataSet;
import org.openstreetmap.josm.data.osm.PrimitiveId;
import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
import org.openstreetmap.josm.gui.HelpAwareOptionPane;
import org.openstreetmap.josm.gui.JosmUserIdentityManager;
import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryDialog;
import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
import org.openstreetmap.josm.gui.help.HelpUtil;
import org.openstreetmap.josm.gui.io.CloseChangesetTask;
import org.openstreetmap.josm.gui.io.DownloadPrimitivesWithReferrersTask;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
import org.openstreetmap.josm.io.ChangesetQuery;
import org.openstreetmap.josm.io.OnlineResource;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.InputMapUtils;
import org.openstreetmap.josm.tools.StreamUtils;
import org.openstreetmap.josm.tools.WindowGeometry;
/**
* ChangesetCacheManager manages the local cache of changesets
* retrieved from the OSM API. It displays both a table of the locally cached changesets
* and detail information about an individual changeset. It also provides actions for
* downloading, querying, closing changesets, in addition to removing changesets from
* the local cache.
* @since 2689
*/
public class ChangesetCacheManager extends JFrame {
/** the unique instance of the cache manager */
private static volatile ChangesetCacheManager instance;
private JTabbedPane pnlChangesetDetailTabs;
/**
* Replies the unique instance of the changeset cache manager
*
* @return the unique instance of the changeset cache manager
*/
public static ChangesetCacheManager getInstance() {
if (instance == null) {
instance = new ChangesetCacheManager();
}
return instance;
}
/**
* Hides and destroys the unique instance of the changeset cache manager.
*
*/
public static void destroyInstance() {
if (instance != null) {
instance.setVisible(true);
instance.dispose();
instance = null;
}
}
private ChangesetCacheManagerModel model;
private JSplitPane spContent;
private boolean needsSplitPaneAdjustment;
private RemoveFromCacheAction actRemoveFromCacheAction;
private CloseSelectedChangesetsAction actCloseSelectedChangesetsAction;
private DownloadSelectedChangesetsAction actDownloadSelectedChangesets;
private DownloadSelectedChangesetContentAction actDownloadSelectedContent;
private DownloadSelectedChangesetObjectsAction actDownloadSelectedChangesetObjects;
private JTable tblChangesets;
/**
* Creates the various models required.
* @return the changeset cache model
*/
static ChangesetCacheManagerModel buildModel() {
DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
return new ChangesetCacheManagerModel(selectionModel);
}
/**
* builds the toolbar panel in the heading of the dialog
*
* @return the toolbar panel
*/
static JPanel buildToolbarPanel() {
JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton btn = new JButton(new QueryAction());
pnl.add(btn);
pnl.add(new SingleChangesetDownloadPanel());
pnl.add(new JButton(new DownloadMyChangesets()));
return pnl;
}
/**
* builds the button panel in the footer of the dialog
*
* @return the button row pane
*/
static JPanel buildButtonPanel() {
JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
//-- cancel and close action
pnl.add(new JButton(new CancelAction()));
//-- help action
pnl.add(new JButton(new ContextSensitiveHelpAction(HelpUtil.ht("/Dialog/ChangesetManager"))));
return pnl;
}
/**
* Builds the panel with the changeset details
*
* @return the panel with the changeset details
*/
protected JPanel buildChangesetDetailPanel() {
JPanel pnl = new JPanel(new BorderLayout());
JTabbedPane tp = new JTabbedPane();
pnlChangesetDetailTabs = tp;
// -- add the details panel
ChangesetDetailPanel pnlChangesetDetail = new ChangesetDetailPanel();
tp.add(pnlChangesetDetail);
model.addPropertyChangeListener(pnlChangesetDetail);
// -- add the tags panel
ChangesetTagsPanel pnlChangesetTags = new ChangesetTagsPanel();
tp.add(pnlChangesetTags);
model.addPropertyChangeListener(pnlChangesetTags);
// -- add the panel for the changeset content
ChangesetContentPanel pnlChangesetContent = new ChangesetContentPanel();
tp.add(pnlChangesetContent);
model.addPropertyChangeListener(pnlChangesetContent);
// -- add the panel for the changeset discussion
ChangesetDiscussionPanel pnlChangesetDiscussion = new ChangesetDiscussionPanel();
tp.add(pnlChangesetDiscussion);
model.addPropertyChangeListener(pnlChangesetDiscussion);
tp.setTitleAt(0, tr("Properties"));
tp.setToolTipTextAt(0, tr("Display the basic properties of the changeset"));
tp.setTitleAt(1, tr("Tags"));
tp.setToolTipTextAt(1, tr("Display the tags of the changeset"));
tp.setTitleAt(2, tr("Content"));
tp.setToolTipTextAt(2, tr("Display the objects created, updated, and deleted by the changeset"));
tp.setTitleAt(3, tr("Discussion"));
tp.setToolTipTextAt(3, tr("Display the public discussion around this changeset"));
pnl.add(tp, BorderLayout.CENTER);
return pnl;
}
/**
* builds the content panel of the dialog
*
* @return the content panel
*/
protected JPanel buildContentPanel() {
JPanel pnl = new JPanel(new BorderLayout());
spContent = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
spContent.setLeftComponent(buildChangesetTablePanel());
spContent.setRightComponent(buildChangesetDetailPanel());
spContent.setOneTouchExpandable(true);
spContent.setDividerLocation(0.5);
pnl.add(spContent, BorderLayout.CENTER);
return pnl;
}
/**
* Builds the table with actions which can be applied to the currently visible changesets
* in the changeset table.
*
* @return changset actions panel
*/
protected JPanel buildChangesetTableActionPanel() {
JPanel pnl = new JPanel(new BorderLayout());
JToolBar tb = new JToolBar(JToolBar.VERTICAL);
tb.setFloatable(false);
// -- remove from cache action
model.getSelectionModel().addListSelectionListener(actRemoveFromCacheAction);
tb.add(actRemoveFromCacheAction);
// -- close selected changesets action
model.getSelectionModel().addListSelectionListener(actCloseSelectedChangesetsAction);
tb.add(actCloseSelectedChangesetsAction);
// -- download selected changesets
model.getSelectionModel().addListSelectionListener(actDownloadSelectedChangesets);
tb.add(actDownloadSelectedChangesets);
// -- download the content of the selected changesets
model.getSelectionModel().addListSelectionListener(actDownloadSelectedContent);
tb.add(actDownloadSelectedContent);
// -- download the objects contained in the selected changesets from the OSM server
model.getSelectionModel().addListSelectionListener(actDownloadSelectedChangesetObjects);
tb.add(actDownloadSelectedChangesetObjects);
pnl.add(tb, BorderLayout.CENTER);
return pnl;
}
/**
* Builds the panel with the table of changesets
*
* @return the panel with the table of changesets
*/
protected JPanel buildChangesetTablePanel() {
JPanel pnl = new JPanel(new BorderLayout());
tblChangesets = new JTable(
model,
new ChangesetCacheTableColumnModel(),
model.getSelectionModel()
);
tblChangesets.addMouseListener(new MouseEventHandler());
InputMapUtils.addEnterAction(tblChangesets, new ShowDetailAction(model));
model.getSelectionModel().addListSelectionListener(new ChangesetDetailViewSynchronizer(model));
// activate DEL on the table
tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "removeFromCache");
tblChangesets.getActionMap().put("removeFromCache", actRemoveFromCacheAction);
pnl.add(new JScrollPane(tblChangesets), BorderLayout.CENTER);
pnl.add(buildChangesetTableActionPanel(), BorderLayout.WEST);
return pnl;
}
protected void build() {
setTitle(tr("Changeset Management Dialog"));
setIconImage(ImageProvider.get("dialogs/changeset", "changesetmanager").getImage());
Container cp = getContentPane();
cp.setLayout(new BorderLayout());
model = buildModel();
actRemoveFromCacheAction = new RemoveFromCacheAction(model);
actCloseSelectedChangesetsAction = new CloseSelectedChangesetsAction(model);
actDownloadSelectedChangesets = new DownloadSelectedChangesetsAction(model);
actDownloadSelectedContent = new DownloadSelectedChangesetContentAction(model);
actDownloadSelectedChangesetObjects = new DownloadSelectedChangesetObjectsAction();
cp.add(buildToolbarPanel(), BorderLayout.NORTH);
cp.add(buildContentPanel(), BorderLayout.CENTER);
cp.add(buildButtonPanel(), BorderLayout.SOUTH);
// the help context
HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/ChangesetManager"));
// make the dialog respond to ESC
InputMapUtils.addEscapeAction(getRootPane(), new CancelAction());
// install a window event handler
addWindowListener(new WindowEventHandler());
}
/**
* Constructs a new {@code ChangesetCacheManager}.
*/
public ChangesetCacheManager() {
build();
}
@Override
public void setVisible(boolean visible) {
if (visible) {
new WindowGeometry(
getClass().getName() + ".geometry",
WindowGeometry.centerInWindow(
getParent(),
new Dimension(1000, 600)
)
).applySafe(this);
needsSplitPaneAdjustment = true;
model.init();
} else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
model.tearDown();
new WindowGeometry(this).remember(getClass().getName() + ".geometry");
}
super.setVisible(visible);
}
/**
* Handler for window events
*
*/
class WindowEventHandler extends WindowAdapter {
@Override
public void windowClosing(WindowEvent e) {
new CancelAction().cancelAndClose();
}
@Override
public void windowActivated(WindowEvent e) {
if (needsSplitPaneAdjustment) {
spContent.setDividerLocation(0.5);
needsSplitPaneAdjustment = false;
}
}
}
/**
* the cancel / close action
*/
static class CancelAction extends AbstractAction {
CancelAction() {
putValue(NAME, tr("Close"));
new ImageProvider("cancel").getResource().attachImageIcon(this);
putValue(SHORT_DESCRIPTION, tr("Close the dialog"));
}
public void cancelAndClose() {
destroyInstance();
}
@Override
public void actionPerformed(ActionEvent e) {
cancelAndClose();
}
}
/**
* The action to query and download changesets
*/
static class QueryAction extends AbstractAction {
QueryAction() {
putValue(NAME, tr("Query"));
new ImageProvider("dialogs", "search").getResource().attachImageIcon(this);
putValue(SHORT_DESCRIPTION, tr("Launch the dialog for querying changesets"));
setEnabled(!Main.isOffline(OnlineResource.OSM_API));
}
@Override
public void actionPerformed(ActionEvent evt) {
Window parent = GuiHelper.getWindowAncestorFor(evt);
if (!GraphicsEnvironment.isHeadless()) {
ChangesetQueryDialog dialog = new ChangesetQueryDialog(parent);
dialog.initForUserInput();
dialog.setVisible(true);
if (dialog.isCanceled())
return;
try {
ChangesetQuery query = dialog.getChangesetQuery();
if (query != null) {
ChangesetCacheManager.getInstance().runDownloadTask(new ChangesetQueryTask(parent, query));
}
} catch (IllegalStateException e) {
Main.error(e);
JOptionPane.showMessageDialog(parent, e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE);
}
}
}
}
/**
* Removes the selected changesets from the local changeset cache
*
*/
static class RemoveFromCacheAction extends AbstractAction implements ListSelectionListener {
private final ChangesetCacheManagerModel model;
RemoveFromCacheAction(ChangesetCacheManagerModel model) {
putValue(NAME, tr("Remove from cache"));
new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this);
putValue(SHORT_DESCRIPTION, tr("Remove the selected changesets from the local cache"));
this.model = model;
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent e) {
ChangesetCache.getInstance().remove(model.getSelectedChangesets());
}
protected void updateEnabledState() {
setEnabled(model.hasSelectedChangesets());
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
/**
* Closes the selected changesets
*
*/
static class CloseSelectedChangesetsAction extends AbstractAction implements ListSelectionListener {
private final ChangesetCacheManagerModel model;
CloseSelectedChangesetsAction(ChangesetCacheManagerModel model) {
putValue(NAME, tr("Close"));
new ImageProvider("closechangeset").getResource().attachImageIcon(this);
putValue(SHORT_DESCRIPTION, tr("Close the selected changesets"));
this.model = model;
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent e) {
Main.worker.submit(new CloseChangesetTask(model.getSelectedChangesets()));
}
protected void updateEnabledState() {
List<Changeset> selected = model.getSelectedChangesets();
JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
for (Changeset cs: selected) {
if (cs.isOpen()) {
if (im.isPartiallyIdentified() && cs.getUser() != null && cs.getUser().getName().equals(im.getUserName())) {
setEnabled(true);
return;
}
if (im.isFullyIdentified() && cs.getUser() != null && cs.getUser().getId() == im.getUserId()) {
setEnabled(true);
return;
}
}
}
setEnabled(false);
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
/**
* Downloads the selected changesets
*
*/
static class DownloadSelectedChangesetsAction extends AbstractAction implements ListSelectionListener {
private final ChangesetCacheManagerModel model;
DownloadSelectedChangesetsAction(ChangesetCacheManagerModel model) {
putValue(NAME, tr("Update changeset"));
new ImageProvider("dialogs/changeset", "updatechangeset").getResource().attachImageIcon(this);
putValue(SHORT_DESCRIPTION, tr("Updates the selected changesets with current data from the OSM server"));
this.model = model;
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent e) {
if (!GraphicsEnvironment.isHeadless()) {
ChangesetCacheManager.getInstance().runDownloadTask(
ChangesetHeaderDownloadTask.buildTaskForChangesets(GuiHelper.getWindowAncestorFor(e), model.getSelectedChangesets()));
}
}
protected void updateEnabledState() {
setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API));
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
/**
* Downloads the content of selected changesets from the OSM server
*
*/
static class DownloadSelectedChangesetContentAction extends AbstractAction implements ListSelectionListener {
private final ChangesetCacheManagerModel model;
DownloadSelectedChangesetContentAction(ChangesetCacheManagerModel model) {
putValue(NAME, tr("Download changeset content"));
new ImageProvider("dialogs/changeset", "downloadchangesetcontent").getResource().attachImageIcon(this);
putValue(SHORT_DESCRIPTION, tr("Download the content of the selected changesets from the server"));
this.model = model;
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent e) {
if (!GraphicsEnvironment.isHeadless()) {
ChangesetCacheManager.getInstance().runDownloadTask(
new ChangesetContentDownloadTask(GuiHelper.getWindowAncestorFor(e), model.getSelectedChangesetIds()));
}
}
protected void updateEnabledState() {
setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API));
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
/**
* Downloads the objects contained in the selected changesets from the OSM server
*/
private class DownloadSelectedChangesetObjectsAction extends AbstractAction implements ListSelectionListener {
DownloadSelectedChangesetObjectsAction() {
putValue(NAME, tr("Download changed objects"));
new ImageProvider("downloadprimitive").getResource().attachImageIcon(this);
putValue(SHORT_DESCRIPTION, tr("Download the current version of the changed objects in the selected changesets"));
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent e) {
if (!GraphicsEnvironment.isHeadless()) {
actDownloadSelectedContent.actionPerformed(e);
Main.worker.submit(() -> {
final List<PrimitiveId> primitiveIds = model.getSelectedChangesets().stream()
.map(Changeset::getContent)
.filter(Objects::nonNull)
.flatMap(content -> StreamUtils.toStream(content::iterator))
.map(ChangesetDataSet.ChangesetDataSetEntry::getPrimitive)
.map(HistoryOsmPrimitive::getPrimitiveId)
.distinct()
.collect(Collectors.toList());
new DownloadPrimitivesWithReferrersTask(false, primitiveIds, true, true, null, null).run();
});
}
}
protected void updateEnabledState() {
setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API));
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
static class ShowDetailAction extends AbstractAction {
private final ChangesetCacheManagerModel model;
ShowDetailAction(ChangesetCacheManagerModel model) {
this.model = model;
}
protected void showDetails() {
List<Changeset> selected = model.getSelectedChangesets();
if (selected.size() == 1) {
model.setChangesetInDetailView(selected.get(0));
}
}
@Override
public void actionPerformed(ActionEvent e) {
showDetails();
}
}
static class DownloadMyChangesets extends AbstractAction {
DownloadMyChangesets() {
putValue(NAME, tr("My changesets"));
new ImageProvider("dialogs/changeset", "downloadchangeset").getResource().attachImageIcon(this);
putValue(SHORT_DESCRIPTION, tr("Download my changesets from the OSM server (max. 100 changesets)"));
setEnabled(!Main.isOffline(OnlineResource.OSM_API));
}
protected void alertAnonymousUser(Component parent) {
HelpAwareOptionPane.showOptionDialog(
parent,
tr("<html>JOSM is currently running with an anonymous user. It cannot download<br>"
+ "your changesets from the OSM server unless you enter your OSM user name<br>"
+ "in the JOSM preferences.</html>"
),
tr("Warning"),
JOptionPane.WARNING_MESSAGE,
HelpUtil.ht("/Dialog/ChangesetManager#CanDownloadMyChangesets")
);
}
@Override
public void actionPerformed(ActionEvent e) {
Window parent = GuiHelper.getWindowAncestorFor(e);
JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
if (im.isAnonymous()) {
alertAnonymousUser(parent);
return;
}
ChangesetQuery query = new ChangesetQuery();
if (im.isFullyIdentified()) {
query = query.forUser(im.getUserId());
} else {
query = query.forUser(im.getUserName());
}
if (!GraphicsEnvironment.isHeadless()) {
ChangesetCacheManager.getInstance().runDownloadTask(new ChangesetQueryTask(parent, query));
}
}
}
class MouseEventHandler extends PopupMenuLauncher {
MouseEventHandler() {
super(new ChangesetTablePopupMenu());
}
@Override
public void mouseClicked(MouseEvent evt) {
if (isDoubleClick(evt)) {
new ShowDetailAction(model).showDetails();
}
}
}
class ChangesetTablePopupMenu extends JPopupMenu {
ChangesetTablePopupMenu() {
add(actRemoveFromCacheAction);
add(actCloseSelectedChangesetsAction);
add(actDownloadSelectedChangesets);
add(actDownloadSelectedContent);
add(actDownloadSelectedChangesetObjects);
}
}
static class ChangesetDetailViewSynchronizer implements ListSelectionListener {
private final ChangesetCacheManagerModel model;
ChangesetDetailViewSynchronizer(ChangesetCacheManagerModel model) {
this.model = model;
}
@Override
public void valueChanged(ListSelectionEvent e) {
List<Changeset> selected = model.getSelectedChangesets();
if (selected.size() == 1) {
model.setChangesetInDetailView(selected.get(0));
} else {
model.setChangesetInDetailView(null);
}
}
}
/**
* Selects the changesets in <code>changests</code>, provided the
* respective changesets are already present in the local changeset cache.
*
* @param changesets the collection of changesets. If {@code null}, the
* selection is cleared.
*/
public void setSelectedChangesets(Collection<Changeset> changesets) {
model.setSelectedChangesets(changesets);
final int idx = model.getSelectionModel().getMinSelectionIndex();
if (idx < 0)
return;
GuiHelper.runInEDTAndWait(() -> tblChangesets.scrollRectToVisible(tblChangesets.getCellRect(idx, 0, true)));
repaint();
}
/**
* Selects the changesets with the ids in <code>ids</code>, provided the
* respective changesets are already present in the local changeset cache.
*
* @param ids the collection of ids. If null, the selection is cleared.
*/
public void setSelectedChangesetsById(Collection<Integer> ids) {
if (ids == null) {
setSelectedChangesets(null);
return;
}
Set<Changeset> toSelect = new HashSet<>();
ChangesetCache cc = ChangesetCache.getInstance();
for (int id: ids) {
if (cc.contains(id)) {
toSelect.add(cc.get(id));
}
}
setSelectedChangesets(toSelect);
}
/**
* Selects the given component in the detail tabbed panel
* @param clazz the class of the component to select
*/
public void setSelectedComponentInDetailPanel(Class<? extends JComponent> clazz) {
for (Component component : pnlChangesetDetailTabs.getComponents()) {
if (component.getClass().equals(clazz)) {
pnlChangesetDetailTabs.setSelectedComponent(component);
break;
}
}
}
/**
* Runs the given changeset download task.
* @param task The changeset download task to run
*/
public void runDownloadTask(final AbstractChangesetDownloadTask task) {
Main.worker.submit(new PostDownloadHandler(task, task.download()));
Main.worker.submit(() -> {
if (task.isCanceled() || task.isFailed())
return;
GuiHelper.runInEDT(() -> setSelectedChangesets(task.getDownloadedData()));
});
}
}