This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit cd6d1919be6fb006f4effe5e087d650b7f79a84b Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Oct 30 00:41:01 2021 +0200 Add a "System monitor" view showing which region of files are read. We use it for monitoring behavior of GeoTIFF reader. --- .../main/java/org/apache/sis/gui/DataViewer.java | 32 +-- .../java/org/apache/sis/gui/SystemMonitor.java | 116 ++++++++ .../main/java/org/apache/sis/gui/package-info.java | 2 +- .../sis/internal/gui/FixedHeaderColumnSize.java | 89 +++++++ .../apache/sis/internal/gui/ResourceLoader.java | 44 +++- .../org/apache/sis/internal/gui/Resources.java | 15 ++ .../apache/sis/internal/gui/Resources.properties | 3 + .../sis/internal/gui/Resources_fr.properties | 3 + .../java/org/apache/sis/internal/gui/Styles.java | 2 +- .../apache/sis/internal/gui/io/FileAccessItem.java | 292 +++++++++++++++++++++ .../apache/sis/internal/gui/io/FileAccessView.java | 136 ++++++++++ .../sis/{gui => internal/gui/io}/package-info.java | 15 +- 12 files changed, 720 insertions(+), 29 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java index bd0eee2..539081d 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java @@ -24,6 +24,7 @@ import java.util.Set; import java.awt.SplashScreen; import javafx.application.Application; import javafx.application.Platform; +import javafx.collections.ObservableList; import javafx.geometry.Rectangle2D; import javafx.scene.Scene; import javafx.scene.control.Menu; @@ -36,7 +37,6 @@ import javafx.stage.FileChooser; import javafx.scene.image.Image; import javafx.stage.Screen; import javafx.stage.Stage; -import org.apache.sis.gui.dataset.LogViewer; import org.apache.sis.gui.dataset.ResourceExplorer; import org.apache.sis.internal.gui.BackgroundThreads; import org.apache.sis.internal.gui.LogHandler; @@ -59,7 +59,7 @@ import org.apache.sis.util.resources.Vocabulary; * * @author Smaniotto Enzo (GSoC) * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * @since 1.1 * @module */ @@ -162,18 +162,20 @@ public class DataViewer extends Application { close.setDisable(!(n instanceof DataStore)); }); } + final Menu windows = new Menu(localized.getString(Resources.Keys.Windows)); + { + final ObservableList<MenuItem> items = windows.getItems(); + content.setWindowsItems(items); + final MenuItem logging = new MenuItem(localized.getString(Resources.Keys.SystemMonitor)); + logging.setOnAction((e) -> showSystemLogsWindow()); + items.addAll(content.createNewWindowMenu(), logging); + } final Menu help = new Menu(localized.getString(Resources.Keys.Help)); { // For keeping variables locale. - final MenuItem logging = new MenuItem(vocabulary.getString(Vocabulary.Keys.Logs)); - logging.setOnAction((e) -> showSystemLogsWindow()); help.getItems().addAll( - localized.menu(Resources.Keys.WebSite, (e) -> getHostServices().showDocument("https://sis.apache.org/")), - new SeparatorMenuItem(), logging, + localized.menu(Resources.Keys.WebSite, (e) -> getHostServices().showDocument("https://sis.apache.org/javafx.html")), localized.menu(Resources.Keys.About, (e) -> AboutDialog.show())); } - final Menu windows = new Menu(localized.getString(Resources.Keys.Windows)); - windows.getItems().add(content.createNewWindowMenu()); - content.setWindowsItems(windows.getItems()); menus.getMenus().addAll(file, windows, help); /* * Set the main content and show. @@ -277,18 +279,10 @@ public class DataViewer extends Application { */ private void showSystemLogsWindow() { if (systemLogsWindow == null) { - final LogViewer viewer = new LogViewer(); - viewer.systemLogs.set(true); - final Stage w = new Stage(); - w.setTitle(Vocabulary.format(Vocabulary.Keys.Logs) + " — Apache SIS"); - w.getIcons().setAll(window.getIcons()); - w.setScene(new Scene(viewer.getView())); - w.setWidth (800); - w.setHeight(600); - window.setOnHidden((e) -> w.hide()); - systemLogsWindow = w; + systemLogsWindow = SystemMonitor.create(window, null); } systemLogsWindow.show(); + systemLogsWindow.toFront(); } /** diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/SystemMonitor.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/SystemMonitor.java new file mode 100644 index 0000000..a29f57d --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/SystemMonitor.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.gui; + +import java.util.Locale; +import java.util.function.UnaryOperator; +import javafx.event.EventHandler; +import javafx.event.EventType; +import javafx.stage.WindowEvent; +import javafx.stage.Stage; +import javafx.scene.Scene; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import org.apache.sis.gui.dataset.LogViewer; +import org.apache.sis.internal.gui.Resources; +import org.apache.sis.internal.gui.ResourceLoader; +import org.apache.sis.internal.gui.io.FileAccessView; +import org.apache.sis.internal.storage.io.ChannelFactory; +import org.apache.sis.util.resources.Vocabulary; + + +/** + * Shows the "System monitor" window. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +final class SystemMonitor implements EventHandler<WindowEvent> { + /** + * The provider of wrappers around channels used for reading data. + * Those wrappers are used for listening to file accesses. + * + * @see ResourceLoader#setFactoryWrapper(UnaryOperator) + */ + private final UnaryOperator<ChannelFactory> listener; + + /** + * Creates new event handler. + */ + private SystemMonitor(final UnaryOperator<ChannelFactory> listener) { + this.listener = listener; + } + + /** + * Invoked when the system monitor window is shown or hidden. + * This method starts or stops listening to read events on channels. + */ + @Override + public void handle(final WindowEvent event) { + final EventType<WindowEvent> type = event.getEventType(); + UnaryOperator<ChannelFactory> wrapper; + if (WindowEvent.WINDOW_SHOWN.equals(type)) { + wrapper = listener; + } else if (WindowEvent.WINDOW_HIDDEN.equals(type)) { + wrapper = null; + } else { + return; + } + ResourceLoader.setFactoryWrapper(wrapper); + } + + /** + * Creates the system monitor window. + * + * @param parent the parent window. + * @param locale the locale, or {@code null} for default. + */ + static Stage create(final Stage parent, final Locale locale) { + final Resources resources = Resources.forLocale(locale); + final Vocabulary vocabulary = Vocabulary.getResources(locale); + final FileAccessView files = new FileAccessView(resources, vocabulary); + final LogViewer logging = new LogViewer(); + logging.systemLogs.set(true); + /* + * Creates the tab pane. + */ + final Tab fileTab = new Tab(resources .getString(Resources.Keys.FileAccesses), files.getView()); + final Tab logTab = new Tab(vocabulary.getString(Vocabulary.Keys.Logs), logging.getView()); + fileTab.setClosable(false); + logTab .setClosable(false); + final TabPane panes = new TabPane(fileTab, logTab); + /* + * Create the window. + */ + final Stage w = new Stage(); + w.setTitle(resources.getString(Resources.Keys.SystemMonitor) + " — Apache SIS"); + w.getIcons().setAll(parent.getIcons()); + w.setScene(new Scene(panes)); + w.setWidth (800); + w.setHeight(600); + /* + * Install listeners. + */ + final SystemMonitor handler = new SystemMonitor(files); + w.setOnShown (handler); + w.setOnHidden(handler); + parent.setOnHidden((e) -> w.hide()); + return w; + } +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java index 8901cca..30f10ab 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java @@ -21,7 +21,7 @@ * @author Smaniotto Enzo (GSoC) * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * @since 1.1 * @module */ diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/FixedHeaderColumnSize.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/FixedHeaderColumnSize.java new file mode 100644 index 0000000..7e8b988 --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/FixedHeaderColumnSize.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.gui; + +import java.util.List; +import javafx.util.Callback; +import javafx.scene.control.TableView; +import javafx.scene.control.TableColumn; + + +/** + * A column resize policy where the size of the first column stay unchanged during table resize event. + * But the column size can still be changed by moving the column separator. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +public final class FixedHeaderColumnSize<E> implements Callback<TableView.ResizeFeatures<E>, Boolean> { + /** + * The singleton instance. + */ + @SuppressWarnings({"rawtypes","unchecked"}) // Partially unchecked for compatibility with JavaFX. + public static final Callback<TableView.ResizeFeatures, Boolean> INSTANCE = new FixedHeaderColumnSize(); + + /** + * Creates the singleton instance. + */ + private FixedHeaderColumnSize() { + } + + /** + * Returns an identifier for this policy. + */ + @Override + public String toString() { + return "fixed-header-resize"; + } + + /** + * Invoked by {@link TableView#resizeColumn(TableColumn, double)} when a column is resized. + * This implementation temporarily freezes the size of the first column (in declaration order, + * not necessarily in visual order) before to resize the other columns. + * + * @param prop the table and the column on which resizing is applied, together with size delta. + * @return whether resizing is allowed. + */ + @Override + public Boolean call(final TableView.ResizeFeatures<E> prop) { + if (prop.getColumn() == null) { + final List<TableColumn<E,?>> columns = prop.getTable().getColumns(); + if (!columns.isEmpty()) { + final TableColumn<E,?> column = columns.get(0); + final boolean reducing = prop.getDelta() < 0; + final double width = column.getWidth(); + final double save = reducing ? column.getMinWidth() : column.getMaxWidth(); + final Boolean result; + try { + column.setMinWidth(width); + column.setMaxWidth(width); + result = TableView.CONSTRAINED_RESIZE_POLICY.call(prop); + } finally { + if (reducing) { + column.setMinWidth(save); + } else { + column.setMaxWidth(save); + } + } + return result; + } + } + return TableView.CONSTRAINED_RESIZE_POLICY.call(prop); + } +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ResourceLoader.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ResourceLoader.java index d18916c..1191b40 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ResourceLoader.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ResourceLoader.java @@ -24,6 +24,7 @@ import java.nio.file.Paths; import java.io.IOException; import java.net.URISyntaxException; import java.util.function.Consumer; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import javafx.concurrent.Task; import javafx.event.EventHandler; @@ -36,7 +37,10 @@ import org.apache.sis.storage.DataStores; import org.apache.sis.util.collection.Cache; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.internal.storage.io.IOUtilities; +import org.apache.sis.internal.storage.io.ChannelFactory; +import org.apache.sis.internal.storage.io.InternalOptionKey; import org.apache.sis.storage.DataStore; +import org.apache.sis.gui.DataViewer; /** @@ -60,7 +64,7 @@ import org.apache.sis.storage.DataStore; * @todo Set title. Add progress listener and cancellation capability. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * * @see BackgroundThreads#execute(Runnable) * @@ -76,6 +80,19 @@ public final class ResourceLoader extends Task<Resource> { private static final Cache<Object,DataStore> CACHE = new Cache<>(); /** + * Provider of wrappers around channels used for reading data. + * Those wrappers can be used for listening to file accesses. + * + * <p><b>Note:</b> it should be a package-private field of {@link DataViewer}, but we put it here because + * we have no public access to {@code DataViewer} non-public members. Using a static field is okay if only + * one {@link DataViewer} application is running in the same JVM.</p> + * + * @see #setFactoryWrapper(UnaryOperator) + * @see DataViewer#getCurrentStage() + */ + private static volatile UnaryOperator<ChannelFactory> factoryWrapper; + + /** * The {@link Resource} input. * This is usually a {@link File} or {@link Path}. */ @@ -150,9 +167,22 @@ public final class ResourceLoader extends Task<Resource> { /** * Loads the resource after we verified that it is not in the cache. + * This method is invoked from a background thread. */ private DataStore load() throws DataStoreException { - return DataStores.open(source); + Object input = source; + final UnaryOperator<ChannelFactory> wrapper = factoryWrapper; + if (wrapper != null) { + final StorageConnector connector; + if (input instanceof StorageConnector) { + connector = (StorageConnector) input; + } else { + connector = new StorageConnector(input); + } + connector.setOption(InternalOptionKey.CHANNEL_FACTORY_WRAPPER, wrapper); + input = connector; + } + return DataStores.open(input); } /** @@ -171,6 +201,16 @@ public final class ResourceLoader extends Task<Resource> { } /** + * Set the provider of wrappers around channels used for reading data. + * Those wrappers can be used for listening to file accesses. + * + * @param wrapper the wrapper, or {@code null} if none. + */ + public static void setFactoryWrapper(final UnaryOperator<ChannelFactory> wrapper) { + factoryWrapper = wrapper; + } + + /** * Removes the given data store from cache and closes it. It is caller's responsibility * to ensure that the given data store is not used anymore before to invoke this method. * This method should be invoked from JavaFX thread for making sure there is no new usage diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java index a2ba112..e9e4b79 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java @@ -61,6 +61,11 @@ public final class Resources extends IndexedResourceBundle { public static final short About = 46; /** + * Accessed regions + */ + public static final short AccessedRegions = 65; + + /** * All files */ public static final short AllFiles = 1; @@ -222,6 +227,11 @@ public final class Resources extends IndexedResourceBundle { public static final short Exit = 20; /** + * File accesses + */ + public static final short FileAccesses = 63; + + /** * Full screen */ public static final short FullScreen = 22; @@ -338,6 +348,11 @@ public final class Resources extends IndexedResourceBundle { public static final short StandardErrorStream = 32; /** + * System monitor + */ + public static final short SystemMonitor = 64; + + /** * Tabular data */ public static final short TabularData = 33; diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties index 561c4a1..5fdf198 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties @@ -21,6 +21,7 @@ # About = About\u2026 +AccessedRegions = Accessed regions AllFiles = All files Along_1 = Along {0} AzimuthalEquidistant = Azimuthal equidistant @@ -53,6 +54,7 @@ ErrorCreatingCRS = Error creating reference system ErrorDataAccess = Error during data access ErrorAt = An error occurred at the following location: Exit = Exit +FileAccesses = File accesses FullScreen = Full screen GeodeticDataset_1 = {0} geodetic dataset GeospatialFiles = Geospatial data files @@ -76,6 +78,7 @@ SelectCrsByContextMenu = For changing the projection, use contextual menu on the SendTo = Send to SizeOrPosition = Size or position StandardErrorStream = Standard error stream +SystemMonitor = System monitor TabularData = Tabular data TileIndexStart = Tile index start UTM = Universal Transverse Mercator diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties index 1213f19..c2bf0d5 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties @@ -26,6 +26,7 @@ # About = \u00c0 propos de\u2026 +AccessedRegions = R\u00e9gions acc\u00e9d\u00e9es AllFiles = Tous les fichiers Along_1 = Selon {0} AzimuthalEquidistant = Azimutal \u00e9quidistant @@ -58,6 +59,7 @@ ErrorCreatingCRS = Erreur \u00e0 la cr\u00e9ation du syst\u00e8me de r\u00 ErrorDataAccess = Erreur lors de l\u2019acc\u00e8s \u00e0 la donn\u00e9e ErrorAt = Une erreur est survenue \u00e0 l\u2019endroit suivant\u00a0: Exit = Quitter +FileAccesses = Acc\u00e8s aux fichiers FullScreen = Plein \u00e9cran GeodeticDataset_1 = Base de donn\u00e9es g\u00e9od\u00e9siques {0} GeospatialFiles = Fichiers de donn\u00e9es g\u00e9ospatiales @@ -81,6 +83,7 @@ SelectCrsByContextMenu = Pour changer la projection, utilisez le menu contextuel SendTo = Envoyer vers SizeOrPosition = Taille ou position StandardErrorStream = Flux d\u2019erreur standard +SystemMonitor = Moniteur syst\u00e8me TabularData = Tableau de valeurs TileIndexStart = D\u00e9but des indices de tuiles UTM = Transverse universelle de Mercator diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java index 075303f..fb244d0 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java @@ -67,7 +67,7 @@ public final class Styles extends Static { /** * "Standard" height of table rows. Can be approximate. */ - public static final double ROW_HEIGHT = 30; + public static final int ROW_HEIGHT = 30; /** * Usual color of text. diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessItem.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessItem.java new file mode 100644 index 0000000..8586781 --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessItem.java @@ -0,0 +1,292 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.gui.io; + +import java.util.List; +import java.util.ListIterator; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.StrokeType; +import org.apache.sis.measure.Range; +import org.apache.sis.util.collection.RangeSet; + + +/** + * A table row with bars showing which parts of a file have been loaded. + * This is a row in the table shown by {@link FileAccessView} table. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +final class FileAccessItem implements Runnable { + /** + * The height of bars in pixels. + */ + private static final int HEIGHT = 16; + + /** + * Number of pixels between cell top border and background border. + */ + private static final int MARGIN_TOP = 1; + + /** + * Number of pixels between cell right border and background border. + */ + private static final int MARGIN_RIGHT = 6; + + /** + * Color to use for filling the rectangles. + */ + private static final Color FILL_COLOR = Color.LIGHTSEAGREEN; + + /** + * Color to use for rectangles border. + */ + private static final Color BORDER_COLOR = FILL_COLOR.darker(); + + /** + * The list of rows shown by the table. + * This is used for removing this item when the file is closed. + */ + final List<FileAccessItem> owner; + + /** + * The text to show in the "File" column. + */ + final String filename; + + /** + * Range of bytes on which a read or write operation has been performed. + */ + private final RangeSet<Long> accessRanges; + + /** + * Visual representation of {@link #accessRanges}. + * The first child is the background rectangle and must be always present. + * All other children are rectangles built from {@link #accessRanges}. + */ + final Pane accessView; + + /** + * Size of the file in bytes. + */ + private long fileSize; + + /** + * Width in pixels of the column were to draw the boxes. + */ + private double columnWidth; + + /** + * Creates a new row in the table of files. + * + * @param owner list where this rows will be added by the caller. + * @param filename text to show in the "File" column. + */ + FileAccessItem(final List<FileAccessItem> owner, final String filename) { + this.owner = owner; + this.filename = filename; + accessRanges = RangeSet.create(Long.class, true, false); + accessView = new Pane(); + /* + * Background rectangle. + */ + final Rectangle background = new Rectangle(); + background.setY(MARGIN_TOP); + background.setHeight(HEIGHT); + background.setStroke(FILL_COLOR.brighter()); + background.setFill(Color.TRANSPARENT); + background.setStrokeType(StrokeType.INSIDE); + accessView.getChildren().add(background); + accessView.widthProperty().addListener((p,o,n) -> { + columnWidth = n.doubleValue() - MARGIN_RIGHT; + adjustSizes(true); + }); + } + + /** + * Reports a read or write operation on a range of bytes. + * This method is invoked by the {@link Observer} wrapper. + * + * @param position offset of the first byte read or written. + * @param count number of bytes read or written. + * @param write {@code false} for a read operation, or {@code true} for a write operation. + */ + private void addRange(final long position, final int count, final boolean write) { + Platform.runLater(() -> { + if (accessRanges.add(position, position + count)) { + adjustSizes(false); + } + }); + } + + /** + * Recomputes all rectangles from current {@link #columnWidth} and {@link #accessRanges}. + * + * @param resized {@code true} if this method is invoked because of a change of column width + * with (presumably) no change in {@link #accessRanges}, or {@code false} if invoked + * after a new range has been added with (presumably) no change in {@link #columnWidth}. + */ + final void adjustSizes(final boolean resized) { + final double scale = columnWidth / fileSize; + if (!Double.isFinite(scale)) { + return; + } + final ObservableList<Node> children = accessView.getChildren(); + final ListIterator<Node> bars = children.listIterator(); + ((Rectangle) bars.next()).setWidth(columnWidth); // Background. + /* + * Adjust the position and width of all rectangles. + */ + for (final Range<Long> range : accessRanges) { + final long min = range.getMinValue(); + final long max = range.getMaxValue(); + final double x = scale * min; + final double width = scale * (max - min); + if (bars.hasNext()) { + final Rectangle r = (Rectangle) bars.next(); + if (resized || r.getX() + r.getWidth() >= x) { + r.setX(x); + r.setWidth(width); + continue; + } + /* + * Newly added range may have merged two or more ranges in a single one. + * Discard all ranges that are fully on the left side of current range. + * This is not really mandatory, but we do that in an effort to keep the + * most "relevant" rectangles (before change) for the new set of ranges. + */ + bars.remove(); + } + final Rectangle r = new Rectangle(x, MARGIN_TOP, width, HEIGHT); + r.setStrokeType(StrokeType.INSIDE); + r.setStroke(BORDER_COLOR); + r.setFill(FILL_COLOR); + bars.add(r); + } + // Remove all remaining children, if any. + children.remove(bars.nextIndex(), children.size()); + } + + /** + * Wrapper around a {@link SeekableByteChannel} which will observe the ranges of bytes read or written. + */ + final class Observer implements SeekableByteChannel { + /** + * The channel doing the actual read or write operations. + */ + private final SeekableByteChannel channel; + + /** + * Creates a new wrapper around the given channel. + */ + Observer(final SeekableByteChannel channel) throws IOException { + this.channel = channel; + fileSize = channel.size(); + } + + /** + * Forwards to the wrapped channel and report the range of bytes read. + */ + @Override + public int read(final ByteBuffer dst) throws IOException { + final long position = position(); + final int count = channel.read(dst); + addRange(position, count, false); + return count; + } + + /** + * Forwards to the wrapped channel and report the range of bytes written. + */ + @Override + public int write(final ByteBuffer src) throws IOException { + final long position = position(); + final int count = channel.write(src); + addRange(position, count, true); + return count; + } + + /** + * Forwards to the wrapper channel. + */ + @Override + public long position() throws IOException { + return channel.position(); + } + + /** + * Forwards to the wrapper channel. + */ + @Override + public SeekableByteChannel position(final long newPosition) throws IOException { + channel.position(newPosition); + return this; + } + + /** + * Forwards to the wrapper channel. + */ + @Override + public long size() throws IOException { + return channel.size(); + } + + /** + * Forwards to the wrapper channel. + */ + @Override + public SeekableByteChannel truncate(final long size) throws IOException { + fileSize = channel.truncate(size).size(); + return this; + } + + /** + * Forwards to the wrapper channel. + */ + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + /** + * Forwards to the wrapper channel and remove the enclosing row from the table. + */ + @Override + public void close() throws IOException { + Platform.runLater(FileAccessItem.this); + channel.close(); + } + } + + /** + * Invoked in JavaFX thread for removing this row from the table. + */ + @Override + public void run() { + owner.remove(this); + } +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessView.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessView.java new file mode 100644 index 0000000..ea7db4a --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessView.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.gui.io; + +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.function.UnaryOperator; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import javafx.scene.control.TableView; +import javafx.scene.control.TableColumn; +import javafx.application.Platform; +import org.apache.sis.gui.Widget; +import org.apache.sis.util.resources.Vocabulary; +import org.apache.sis.internal.gui.FixedHeaderColumnSize; +import org.apache.sis.internal.gui.ImmutableObjectProperty; +import org.apache.sis.internal.gui.Resources; +import org.apache.sis.internal.storage.io.ChannelFactory; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.event.StoreListeners; + + +/** + * A table of filenames associated with bars showing which parts of the files have been read or written. + * New rows are added when files are opened and removed when files are closed. + * + * <p>The {@link org.apache.sis.gui.SystemMonitor} class will track only channels opened while the + * "System monitor" window was visible. This policy is for avoiding the cost of tracking operations + * in the vast majority of cases when user has no interest in those information.</p> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +public final class FileAccessView extends Widget implements UnaryOperator<ChannelFactory> { + /** + * The table where opened files are listed. + */ + private final TableView<FileAccessItem> table; + + /** + * Creates a new widget. + * + * @param resources localized resources, provided because already known by caller. + * @param vocabulary localized resources, provided because already known by caller. + */ + public FileAccessView(final Resources resources, final Vocabulary vocabulary) { + final TableColumn<FileAccessItem, String> filenameColumn; + final TableColumn<FileAccessItem,Pane> accessColumn; + filenameColumn = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.File)); + accessColumn = new TableColumn<>(resources.getString(Resources.Keys.AccessedRegions)); + accessColumn .setSortable(false); + accessColumn .setMinWidth (120); + filenameColumn.setMinWidth ( 80); + filenameColumn.setPrefWidth(200); + filenameColumn.setCellValueFactory((cell) -> new ImmutableObjectProperty<>(cell.getValue().filename)); + accessColumn .setCellValueFactory((cell) -> new ImmutableObjectProperty<>(cell.getValue().accessView)); + table = new TableView<>(); + table.setColumnResizePolicy(FixedHeaderColumnSize.INSTANCE); + table.getColumns().setAll(filenameColumn, accessColumn); + } + + /** + * Returns the node to show in a window. + * + * @return the node to show. + */ + @Override + public Region getView() { + return table; + } + + /** + * Invoked when a new {@link ReadableByteChannel} or {@link WritableByteChannel} is about to be created. + * The caller will replace the given factory by the returned factory. It allows us to wrap the channel + * in an object will will collect information about blocks read. + * + * @param factory the factory for creating channels. + * @return the factory to use instead of the factory given in argument. + */ + @Override + public ChannelFactory apply(final ChannelFactory factory) { + return new ChannelFactory() { + /** + * Creates a readable channel and listens (if possible) read operations. + * Current implementation listens only to {@link SeekableByteChannel} + * because otherwise we do not know the file size. + * + * @param filename data store name. + * @param listeners set of registered {@code StoreListener}s for the data store, or {@code null} if none. + * @return the channel for the given input. + * @throws DataStoreException if the channel is read-once. + * @throws IOException if an error occurred while opening the channel. + */ + @Override + public ReadableByteChannel readable(final String filename, final StoreListeners listeners) + throws DataStoreException, IOException + { + final ReadableByteChannel channel = factory.readable(filename, listeners); + if (channel instanceof SeekableByteChannel) { + final FileAccessItem item = new FileAccessItem(table.getItems(), filename); + Platform.runLater(() -> item.owner.add(item)); + return item.new Observer((SeekableByteChannel) channel); + } + return channel; + } + + /** + * Forwards to the factory (listeners not yet implemented). + */ + @Override + public WritableByteChannel writable(final String filename, final StoreListeners listeners) + throws DataStoreException, IOException + { + return factory.writable(filename, listeners); + } + }; + } +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/package-info.java similarity index 71% copy from application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java copy to application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/package-info.java index 8901cca..5b0fcfb 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/package-info.java @@ -16,13 +16,16 @@ */ /** - * JavaFX application for Apache SIS. + * Widgets related to I/O operations, together with classes for listening to I/O activity. + * + * <STRONG>Do not use!</STRONG> + * + * This package is for internal use by SIS only. Classes in this package + * may change in incompatible ways in any future version without notice. * - * @author Smaniotto Enzo (GSoC) - * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.1 - * @since 1.1 + * @version 1.2 + * @since 1.2 * @module */ -package org.apache.sis.gui; +package org.apache.sis.internal.gui.io;