commit 02e140ac9140f6bf638fac97730d848a52dcb7ea Author: Gerardo Marx Date: Mon Nov 29 12:45:23 2021 -0600 first commit diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..933d8a2 --- /dev/null +++ b/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..34914aa --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + IJ + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..af07d5f --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/IJ_Props.txt b/IJ_Props.txt new file mode 100644 index 0000000..a58cd47 --- /dev/null +++ b/IJ_Props.txt @@ -0,0 +1,466 @@ + # IJ_Props.txt - This is the ImageJ properties file. ImageJ uses +# the information in this file to install plug-ins in menus. +# ImageJ looks for this file in ij.jar. It can be edited by +# opening ij.jar with a ZIP utility. + +# Note that commands must be unique. + +# Version 1.53 + +# Commands installed in the right-click popup menu +# May be overridden in StartupMacros +popup01=Show Info... +popup02=Properties... +popup03=Rename... +popup04=Measure +popup05=Histogram +popup06=Duplicate Image... +popup07=Original Scale +popup08=- +popup09=Record... +popup10=Find Commands... +popup11=Capture Screen + +# Plugins installed in the File/New submenu +new01="Image...[n]",ij.plugin.Commands("new") +new02="Hyperstack...",ij.plugin.HyperStackMaker +new03="Text Window[N]",ij.plugin.NewPlugin("text") +new04="Internal Clipboard",ij.plugin.Clipboard("show") +new05="System Clipboard[V]",ij.plugin.Clipboard("showsys") + +# Plugins installed in the File/Import submenu +import01="Image Sequence...",ij.plugin.FolderOpener +import02="Raw...",ij.plugin.Raw +import03="LUT... ",ij.plugin.LutLoader +import04="Text Image... ",ij.plugin.TextReader +import05="Text File... ",ij.plugin.TextFileReader +import06="Results... ",ij.plugin.SimpleCommands("import") +import07="Table... ",ij.plugin.SimpleCommands("table") +import08="URL...",ij.plugin.URLOpener +import09="Stack From List...",ij.plugin.ListVirtualStack +import10="TIFF Virtual Stack...",ij.plugin.FileInfoVirtualStack +import11="AVI...",ij.plugin.AVI_Reader +import12="XY Coordinates... ",ij.plugin.XY_Reader +#import08="TWAIN...",ij.plugin.twain.Twain +#import09="Capture Video...",QT_Capture +#import10="QuickTime Movie...",Movie_Opener +#import11="Pict...",QuickTime_Opener + +# Plugins installed in the File/Save As submenu +save01="Tiff...",ij.plugin.filter.Writer("tiff") +save02="Gif...",ij.plugin.filter.Writer("gif") +save03="Jpeg...",ij.plugin.filter.Writer("jpeg") +save04="Text Image...",ij.plugin.filter.Writer("text") +save05="ZIP...",ij.plugin.filter.Writer("zip") +save06="Raw Data...",ij.plugin.filter.Writer("raw") +save07="Image Sequence... ",ij.plugin.StackWriter +save08="AVI... ",ij.plugin.filter.AVI_Writer +save09="BMP...",ij.plugin.filter.Writer("bmp") +save10="PNG...",ij.plugin.filter.Writer("png") +save11="PGM...",ij.plugin.filter.Writer("pgm") +save12="FITS...",ij.plugin.filter.Writer("fits") +save13="LUT...",ij.plugin.filter.Writer("lut") +save14="Selection...",ij.plugin.filter.RoiWriter +save15="XY Coordinates...",ij.plugin.filter.XYWriter +save16="Results...",ij.plugin.MeasurementsWriter +save17="Text...",ij.plugin.TextWriter +#save18="QuickTime Movie... ",QT_Movie_Writer + +# Plugins installed in the Edit/Selection submenu +selection01="Select All[a]",ij.plugin.Selection("all") +selection02="Select None[A]",ij.plugin.Selection("none") +selection03="Restore Selection[E]",ij.plugin.Selection("restore") +selection04="Fit Spline",ij.plugin.Selection("spline") +selection05="Fit Circle",ij.plugin.Selection("circle") +selection06="Fit Ellipse",ij.plugin.Selection("ellipse") +selection07="Fit Rectangle",ij.plugin.Selection("rect") +selection08="Interpolate",ij.plugin.Selection("interpolate") +selection09="Convex Hull",ij.plugin.Selection("hull") +selection10="Make Inverse",ij.plugin.Selection("inverse") +selection11="Create Selection",ij.plugin.Selection("from") +selection12="Create Mask",ij.plugin.Selection("mask") +selection13=- +selection14="Properties... [y]",ij.plugin.Selection("properties") +selection15="Scale... ",ij.plugin.RoiScaler +selection16="Rotate...",ij.plugin.Selection("rotate") +selection17="Enlarge...",ij.plugin.RoiEnlarger +selection18="Make Band...",ij.plugin.Selection("band") +selection19="Specify...",ij.plugin.SpecifyROI +selection20="Straighten...",ij.plugin.Straightener +selection21="To Bounding Box",ij.plugin.Selection("tobox") +selection22="Line to Area",ij.plugin.Selection("toarea") +selection23="Area to Line",ij.plugin.Selection("toline") +selection24="Image to Selection...",ij.plugin.OverlayCommands("image-roi") +selection25="Add to Manager[t]",ij.plugin.Selection("add") + +# Plugins installed in the Edit/Options submenu +options01="Appearance...",ij.plugin.AppearanceOptions +options02="Arrow Tool...",ij.plugin.ArrowToolOptions +options03="Colors...",ij.plugin.Colors +options04="Compiler...",ij.plugin.Compiler("options") +options05="Conversions...",ij.plugin.Options("conv") +options06="DICOM...",ij.plugin.Options("dicom") +options07="Fonts...",ij.plugin.SimpleCommands("fonts") +options08="Input/Output...",ij.plugin.Options("io") +options09="Line Width...",ij.plugin.Options("line") +options10="Memory & Threads...",ij.plugin.Memory +options11="Misc...",ij.plugin.Options("misc") +options12="Plots...",ij.plugin.Profiler("set") +options13="Point Tool...",ij.plugin.PointToolOptions +options14="Proxy Settings...",ij.plugin.ProxySettings +options15="Roi Defaults...",ij.gui.RoiDefaultsDialog +options16="Rounded Rect Tool...",ij.plugin.RectToolOptions +options17="Startup...",ij.plugin.Startup +options18="Wand Tool...",ij.plugin.WandToolOptions +options19=- +options20="Fresh Start",ij.plugin.Options("fresh-start") +options21="Reset... ",ij.plugin.Options("reset") + +# Plugins installed in the Image/Adjust submenu +adjust01="Brightness/Contrast...[C]",ij.plugin.frame.ContrastAdjuster +adjust02="Window/Level...",ij.plugin.frame.ContrastAdjuster("wl") +adjust03="Color Balance...",ij.plugin.frame.ContrastAdjuster("balance") +adjust04="Threshold...[T]",ij.plugin.frame.ThresholdAdjuster +adjust05="Color Threshold...",ij.plugin.frame.ColorThresholder +adjust06="Size...",ij.plugin.Resizer +adjust07="Canvas Size...",ij.plugin.CanvasResizer +adjust08="Line Width... ",ij.plugin.frame.LineWidthAdjuster +adjust09="Coordinates...",ij.plugin.Coordinates + + +# Plugins installed in the Image/Color submenu +color01="Split Channels",ij.plugin.ChannelSplitter +color02="Merge Channels...",ij.plugin.RGBStackMerge +color03="Arrange Channels...",ij.plugin.ChannelArranger +color04="Channels Tool...[Z]",ij.plugin.frame.Channels +color05=- +color06="Stack to RGB",ij.plugin.RGBStackConverter +color07="Make Composite",ij.plugin.CompositeConverter +color08="Show LUT",ij.plugin.filter.LutViewer +color09="Display LUTs",ij.plugin.SimpleCommands("display") +color10="Edit LUT...",ij.plugin.LUT_Editor +color11="Color Picker...[K]",ij.plugin.frame.ColorPicker + +# Plugins installed in the Image/Stacks submenu +stacks01="Add Slice",ij.plugin.StackEditor("add") +stacks02="Delete Slice",ij.plugin.StackEditor("delete") +stacks03="Next Slice [>]",ij.plugin.Animator("next") +stacks04="Previous Slice [<]",ij.plugin.Animator("previous") +stacks05="Set Slice...",ij.plugin.Animator("set") +stacks06=- +stacks07="Images to Stack",ij.plugin.ImagesToStack +stacks08="Stack to Images",ij.plugin.StackEditor("toimages") +stacks09="Make Montage...",ij.plugin.MontageMaker +stacks10="Reslice [/]...",ij.plugin.Slicer +stacks11="Orthogonal Views[H]",ij.plugin.Orthogonal_Views +stacks12="Z Project...",ij.plugin.ZProjector +stacks13="3D Project...",ij.plugin.Projector +stacks14="Plot Z-axis Profile",ij.plugin.ZAxisProfiler +stacks15="Measure Stack...",ij.plugin.SimpleCommands("measure") +stacks16="Label...",ij.plugin.filter.StackLabeler +stacks17="Statistics",ij.plugin.Stack_Statistics + +# Plugins installed in the Image/Stacks/Animation submenu +animation_01="Start Animation [\\]",ij.plugin.Animator("start") +animation_02="Stop Animation",ij.plugin.Animator("stop") +animation_03="Animation Options...",ij.plugin.Animator("options") + +# Plugins installed in the Image/Stacks/Tools submenu +tools_01="Combine...",ij.plugin.StackCombiner +tools_02="Concatenate...",ij.plugin.Concatenator +tools_03="Grouped Z Project...",ij.plugin.GroupedZProjector +tools_04="Insert...",ij.plugin.StackInserter +tools_05="Magic Montage Tools",ij.plugin.SimpleCommands("magic") +tools_06="Make Substack...",ij.plugin.SubstackMaker +tools_07="Montage to Stack...",ij.plugin.StackMaker +tools_08="Plot XY Profile",ij.plugin.StackPlotter +tools_09="Reduce...",ij.plugin.StackReducer +tools_10="Remove Slice Labels",ij.plugin.SimpleCommands("remove") +tools_11="Reverse",ij.plugin.StackReverser +tools_12="Set Label...",ij.plugin.SimpleCommands("set") + +# Plugins installed in the Image/Hyperstacks submenu +hyperstacks01="New Hyperstack...",ij.plugin.HyperStackMaker +hyperstacks02="Stack to Hyperstack...",ij.plugin.HyperStackConverter("stacktohs") +hyperstacks03="Hyperstack to Stack",ij.plugin.HyperStackConverter("hstostack") +hyperstacks04="Reduce Dimensionality...",ij.plugin.HyperStackReducer + +# Plugins installed in the Image/Transform submenu +transform01="Flip Horizontally",ij.plugin.filter.Transformer("fliph") +transform02="Flip Vertically",ij.plugin.filter.Transformer("flipv") +transform03="Flip Z",ij.plugin.StackReverser +transform04="Rotate 90 Degrees Right",ij.plugin.filter.Transformer("right") +transform05="Rotate 90 Degrees Left",ij.plugin.filter.Transformer("left") +transform06="Rotate... ",ij.plugin.filter.Rotator +transform07="Translate...",ij.plugin.filter.Translator +transform08="Bin...",ij.plugin.Binner +transform09=- +transform10="Image to Results",ij.plugin.SimpleCommands("itor") +transform11="Results to Image",ij.plugin.SimpleCommands("rtoi") + +# Plugins installed in the Image/Zoom submenu +zoom01="In [+]",ij.plugin.Zoom("in") +zoom02="Out [-]",ij.plugin.Zoom("out") +zoom03="Original Scale[4]",ij.plugin.Zoom("orig") +zoom04="View 100%[5]",ij.plugin.Zoom("100%") +zoom05="To Selection",ij.plugin.Zoom("to") +zoom06="Scale to Fit",ij.plugin.Zoom("scale") +zoom07="Set... ",ij.plugin.Zoom("set") +zoom08="Maximize",ij.plugin.Zoom("max") + +# Plugins installed in the Image/Overlay submenu +overlay01="Add Selection...[b]",ij.plugin.OverlayCommands("add") +overlay02="Add Image...",ij.plugin.OverlayCommands("image") +overlay03="Hide Overlay",ij.plugin.OverlayCommands("hide") +overlay04="Show Overlay",ij.plugin.OverlayCommands("show") +overlay05="From ROI Manager",ij.plugin.OverlayCommands("from") +overlay06="To ROI Manager",ij.plugin.OverlayCommands("to") +overlay07="Remove Overlay",ij.plugin.OverlayCommands("remove") +overlay08="List Elements",ij.plugin.OverlayCommands("list") +overlay09="Flatten[F]",ij.plugin.OverlayCommands("flatten") +overlay10="Labels...",ij.plugin.OverlayLabels +overlay11="Measure Overlay",ij.plugin.OverlayCommands("measure") +overlay12="Overlay Options...[Y]",ij.plugin.OverlayCommands("options") + +# Plugins installed in the Image/Lookup Tables submenu +lookup01="Invert LUT",ij.plugin.LutLoader("invert") +lookup02="Apply LUT",ij.plugin.filter.LutApplier +lookup03=- +lookup04="Fire",ij.plugin.LutLoader("fire") +lookup05="Grays",ij.plugin.LutLoader("grays") +lookup06="Ice",ij.plugin.LutLoader("ice") +lookup07="Spectrum",ij.plugin.LutLoader("spectrum") +lookup08="3-3-2 RGB",ij.plugin.LutLoader("3-3-2 RGB") +lookup09="Red",ij.plugin.LutLoader("red") +lookup10="Green",ij.plugin.LutLoader("green") +lookup11="Blue",ij.plugin.LutLoader("blue") +lookup12="Cyan",ij.plugin.LutLoader("cyan") +lookup13="Magenta",ij.plugin.LutLoader("magenta") +lookup14="Yellow",ij.plugin.LutLoader("yellow") +lookup15="Red/Green",ij.plugin.LutLoader("redgreen") + +# Plug-ins installed in the Process/Noise submenu +noise01="Add Noise",ij.plugin.filter.Filters("add") +noise02="Add Specified Noise...",ij.plugin.filter.Filters("noise") +noise03="Salt and Pepper",ij.plugin.filter.SaltAndPepper +noise04=- +noise05="Despeckle",ij.plugin.filter.RankFilters("despeckle") +noise06="Remove Outliers...",ij.plugin.filter.RankFilters("outliers") +noise07="Remove NaNs...",ij.plugin.filter.RankFilters("nan") + +# Plugins installed in the Process/Shadows submenu +shadows01="North",ij.plugin.filter.Shadows("north") +shadows02="Northeast",ij.plugin.filter.Shadows("northeast") +shadows03="East",ij.plugin.filter.Shadows("east") +shadows04="Southeast",ij.plugin.filter.Shadows("southeast") +shadows05="South",ij.plugin.filter.Shadows("south") +shadows06="Southwest",ij.plugin.filter.Shadows("southwest") +shadows07="West",ij.plugin.filter.Shadows("west") +shadows08="Northwest",ij.plugin.filter.Shadows("northwest") +shadows09=- +shadows10="Shadows Demo",ij.plugin.filter.Shadows("demo") + +# Plugins installed in the Process/Binary submenu +binary01="Make Binary",ij.plugin.Thresholder +binary02="Convert to Mask",ij.plugin.Thresholder("mask") +binary03=- +binary04="Erode",ij.plugin.filter.Binary("erode") +binary05="Dilate",ij.plugin.filter.Binary("dilate") +binary06="Open",ij.plugin.filter.Binary("open") +# Can't use "Close" because it conflicts with File/Close +binary07="Close-",ij.plugin.filter.Binary("close") +binary08=- +binary09="Outline",ij.plugin.filter.Binary("outline") +binary10="Fill Holes",ij.plugin.filter.Binary("fill") +binary11="Skeletonize",ij.plugin.filter.Binary("skel") +binary12=- +binary13="Distance Map",ij.plugin.filter.EDM("edm") +binary14="Ultimate Points",ij.plugin.filter.EDM("points") +binary15="Watershed",ij.plugin.filter.EDM("watershed") +binary16="Voronoi",ij.plugin.filter.EDM("voronoi") +binary17=- +binary18="Options...",ij.plugin.filter.Binary("options") + +# Plugins installed in the Process/Math submenu +math01="Add...",ij.plugin.filter.ImageMath("add") +math02="Subtract...",ij.plugin.filter.ImageMath("sub") +math03="Multiply...",ij.plugin.filter.ImageMath("mul") +math04="Divide...",ij.plugin.filter.ImageMath("div") +math05="AND...",ij.plugin.filter.ImageMath("and") +math06="OR...",ij.plugin.filter.ImageMath("or") +math07="XOR...",ij.plugin.filter.ImageMath("xor") +math08="Min...",ij.plugin.filter.ImageMath("min") +math09="Max...",ij.plugin.filter.ImageMath("max") +math10="Gamma...",ij.plugin.filter.ImageMath("gamma") +math11="Set...",ij.plugin.filter.ImageMath("set") +math12="Log",ij.plugin.filter.ImageMath("log") +math13="Exp",ij.plugin.filter.ImageMath("exp") +math14="Square",ij.plugin.filter.ImageMath("sqr") +math15="Square Root",ij.plugin.filter.ImageMath("sqrt") +math16="Reciprocal",ij.plugin.filter.ImageMath("reciprocal") +math17="NaN Background",ij.plugin.filter.ImageMath("nan") +math18="Abs",ij.plugin.filter.ImageMath("abs") +math19="Macro...",ij.plugin.filter.ImageMath("macro") + +# Plugins installed in the Process/FFT submenu +fft01="FFT",ij.plugin.FFT("fft") +fft02="Inverse FFT",ij.plugin.FFT("inverse") +fft03="Redisplay Power Spectrum",ij.plugin.FFT("redisplay") +fft04="FFT Options...",ij.plugin.FFT("options") +fft05=- +fft06="Bandpass Filter...",ij.plugin.filter.FFTFilter +fft07="Custom Filter...",ij.plugin.filter.FFTCustomFilter +fft08="FD Math...",ij.plugin.FFTMath +fft09="Swap Quadrants",ij.plugin.FFT("swap") +fft10="Make Circular Selection...",ij.plugin.CircularRoiMaker + +# Plugins installed in the Process/Filters submenu +filters01="Convolve...",ij.plugin.filter.Convolver +filters02="Gaussian Blur...",ij.plugin.filter.GaussianBlur +filters03="Median...",ij.plugin.filter.RankFilters("median") +filters04="Mean...",ij.plugin.filter.RankFilters("mean") +filters05="Minimum...",ij.plugin.filter.RankFilters("min") +filters06="Maximum...",ij.plugin.filter.RankFilters("max") +filters07="Unsharp Mask...",ij.plugin.filter.UnsharpMask +filters08="Variance...",ij.plugin.filter.RankFilters("variance") +filters09="Top Hat...",ij.plugin.filter.RankFilters("tophat") +filters10=- +filters11="Gaussian Blur 3D...",ij.plugin.GaussianBlur3D +filters12="Median 3D...",ij.plugin.Filters3D("median") +filters13="Mean 3D...",ij.plugin.Filters3D("mean") +filters14="Minimum 3D...",ij.plugin.Filters3D("min") +filters15="Maximum 3D...",ij.plugin.Filters3D("max") +filters16="Variance 3D...",ij.plugin.Filters3D("var") +filters17=- +filters18="Show Circular Masks...",ij.plugin.filter.RankFilters("masks") + +# Plugins installed in the File/Batch submenu +batch01="Measure...",ij.plugin.BatchMeasure +batch02="Convert...",ij.plugin.BatchConverter +batch03="Macro... ",ij.plugin.BatchProcessor +batch04="Virtual Stack...",ij.plugin.BatchProcessor("stack") + +# Plugins installed in the Analyze/Gels submenu +gels01="Select First Lane[1]",ij.plugin.GelAnalyzer("first") +gels02="Select Next Lane[2]",ij.plugin.GelAnalyzer("next") +gels03="Plot Lanes[3]",ij.plugin.GelAnalyzer("plot") +gels04="Re-plot Lanes",ij.plugin.GelAnalyzer("replot") +gels05="Reset",ij.plugin.GelAnalyzer("reset") +gels06="Label Peaks",ij.plugin.GelAnalyzer("label") +gels07="Gel Analyzer Options...",ij.plugin.GelAnalyzer("options") + +# Plugins installed in the Analyze/Tools submenu +tools01="Save XY Coordinates...",ij.plugin.XYCoordinates +tools02="Fractal Box Count...",ij.plugin.filter.FractalBoxCounter +tools03="Analyze Line Graph",ij.plugin.filter.LineGraphAnalyzer +tools04="Curve Fitting...",ij.plugin.frame.Fitter +tools05="ROI Manager...",ij.plugin.frame.RoiManager +tools06="Scale Bar...",ij.plugin.ScaleBar +tools07="Calibration Bar...",ij.plugin.CalibrationBar +tools08="Synchronize Windows",ij.plugin.frame.SyncWindows +tools09="Grid...",ij.plugin.Grid + +# Plugins installed in the Plugins menu +plug-in01=>"Macros" +plug-in02=>"Shortcuts" +plug-in03=>"Utilities" +plug-in04=>"New_" +plug-in05="Compile and Run...",ij.plugin.Compiler +plug-in06="Install... [M]",ij.plugin.PluginInstaller +#plug-in07=- +#plug-in08=>"User_Plugins" + +# Install user plugins located in ij.jar to Plugins>User Plugins submenu +#user_plugins01="Red And Blue",RedAndBlue_ +#user_plugins02="Inverter",Inverter_ + + +# Plugins installed in the Plugins/Macros submenu +# 'MACROS_MENU_COMMANDS' in MacroInstaller must be updated when items are added +macros01="Install...",ij.plugin.MacroInstaller +macros02="Run...",ij.plugin.Macro_Runner +macros03="Edit...",ij.plugin.Compiler("edit") +macros04="Startup Macros...",ij.plugin.Commands("startup") +macros05="Interactive Interpreter...[j]",ij.plugin.SimpleCommands("interactive") +macros06="Record...",ij.plugin.frame.Recorder +macros07=- + +# Plugins installed in the Plugins/Shortcuts submenu +shortcuts01="Add Shortcut... ",ij.plugin.Hotkeys("install") +shortcuts02="Add Shortcut by Name... ",ij.plugin.Hotkeys("install2") +shortcuts03="Remove Shortcut...",ij.plugin.Hotkeys("remove") +shortcuts04="List Shortcuts",ij.plugin.CommandLister("shortcuts") +shortcuts05="List Commands",ij.plugin.Hotkeys("list") +shortcuts06=- + +# Plugins installed in the Plugins/Utilities submenu +utilities01="Control Panel...[U]",ij.plugin.ControlPanel +utilities02="Find Commands...[l]",ij.plugin.CommandFinder +utilities03="Commands...[L]",ij.plugin.frame.Commands +utilities04="Search...",ij.plugin.SimpleCommands("search") +utilities05="Monitor Events...",ij.plugin.EventListener +utilities06="Monitor Memory...",ij.plugin.frame.MemoryMonitor +utilities07=- +utilities08="Capture Screen[G]",ij.plugin.ScreenGrabber +utilities09="Capture Delayed...",ij.plugin.ScreenGrabber("delay") +utilities10="Capture Image",ij.plugin.ScreenGrabber("image") +utilities11=- +utilities12="ImageJ Properties",ij.plugin.JavaProperties +utilities13="Threads",ij.plugin.ThreadLister +utilities14="Benchmark",ij.plugin.Benchmark +utilities15="Reset...",ij.plugin.SimpleCommands("reset") + +# Plugins installed in the Plugins/New submenu +new_01="Macro",ij.plugin.NewPlugin("macro") +new_02="Macro Tool",ij.plugin.NewPlugin("macro-tool") +new_03="JavaScript",ij.plugin.NewPlugin("javascript") +new_04=- +new_05="Plugin",ij.plugin.NewPlugin("plugin") +new_06="Plugin Filter",ij.plugin.NewPlugin("filter") +new_07="Plugin Frame",ij.plugin.NewPlugin("frame") +new_08="Plugin Tool",ij.plugin.NewPlugin("plugin-tool") +new_09=- +new_10="Text Window...",ij.plugin.NewPlugin("text+dialog") + +# Plugins installed in the Help/About submenu +about01="About This Submenu...",ij.plugin.SimpleCommands("about") +about02=- + +# URL of directory containing the sample images +# location2 is uses with Java 6, which does not support https +images.location=https://imagej.net/images/ +images.location2=http://imagej.net/images/ + +# Images installed in the Open Samples submenu +# RawReader expects a string with "name width height nImages bitsPerPixel offset [white]" +open01="AuPbSn 40",ij.plugin.URLOpener("AuPbSn40.jpg") +open02="Bat Cochlea Volume",ij.plugin.URLOpener("bat-cochlea-volume.zip") +open03="Bat Cochlea Renderings",ij.plugin.URLOpener("bat-cochlea-renderings.zip") +open04="Blobs[B]",ij.plugin.URLOpener("blobs.gif") +open05="Boats",ij.plugin.URLOpener("boats.gif") +open06="Cardio (RGB DICOM)",ij.plugin.URLOpener("cardio.dcm.zip") +open07="Cell Colony",ij.plugin.URLOpener("Cell_Colony.jpg") +open08="Clown",ij.plugin.URLOpener("clown.jpg") +open09="Confocal Series",ij.plugin.URLOpener("confocal-series.zip") +open10="CT (16-bit DICOM)",ij.plugin.URLOpener("ct.dcm.zip") +open11="Dot Blot",ij.plugin.URLOpener("Dot_Blot.jpg") +open12="Embryos",ij.plugin.URLOpener("embryos.jpg") +open13="Fluorescent Cells",ij.plugin.URLOpener("FluorescentCells.zip") +open14="Fly Brain",ij.plugin.URLOpener("flybrain.zip") +open15="Gel",ij.plugin.URLOpener("gel.gif") +open16="HeLa Cells (48-bit RGB)",ij.plugin.URLOpener("hela-cells.zip") +open17="Image with Overlay",ij.plugin.URLOpener("ImageWithOverlay.zip") +open18="Leaf",ij.plugin.URLOpener("leaf.jpg") +open19="Line Graph",ij.plugin.URLOpener("LineGraph.jpg") +open20="Mitosis (5D stack)",ij.plugin.URLOpener("Spindly-GFP.zip") +open21="MRI Stack",ij.plugin.URLOpener("mri-stack.zip") +open22="M51 Galaxy (16-bits)",ij.plugin.URLOpener("m51.zip") +open23="Neuron (5 channels)",ij.plugin.URLOpener("Rat_Hippocampal_Neuron.zip") +open24="Nile Bend",ij.plugin.URLOpener("NileBend.jpg") +open25="Organ of Corti (4D stack)",ij.plugin.URLOpener("organ-of-corti.zip") +open26="Particles",ij.plugin.URLOpener("particles.gif") +open27="T1 Head (16-bits)",ij.plugin.URLOpener("t1-head.zip") +open28="T1 Head Renderings",ij.plugin.URLOpener("t1-rendering.zip") +open29="TEM Filter",ij.plugin.URLOpener("TEM_filter_sample.jpg") +open30="Tree Rings",ij.plugin.URLOpener("Tree_Rings.jpg") + diff --git a/bin/ij/CommandListener.class b/bin/ij/CommandListener.class new file mode 100644 index 0000000..bbea16c Binary files /dev/null and b/bin/ij/CommandListener.class differ diff --git a/bin/ij/CompositeImage.class b/bin/ij/CompositeImage.class new file mode 100644 index 0000000..592b086 Binary files /dev/null and b/bin/ij/CompositeImage.class differ diff --git a/bin/ij/Executer.class b/bin/ij/Executer.class new file mode 100644 index 0000000..267b9e8 Binary files /dev/null and b/bin/ij/Executer.class differ diff --git a/bin/ij/IJ$ExceptionHandler.class b/bin/ij/IJ$ExceptionHandler.class new file mode 100644 index 0000000..be6c545 Binary files /dev/null and b/bin/ij/IJ$ExceptionHandler.class differ diff --git a/bin/ij/IJ.class b/bin/ij/IJ.class new file mode 100644 index 0000000..7856c34 Binary files /dev/null and b/bin/ij/IJ.class differ diff --git a/bin/ij/IJEventListener.class b/bin/ij/IJEventListener.class new file mode 100644 index 0000000..7fbcbdb Binary files /dev/null and b/bin/ij/IJEventListener.class differ diff --git a/bin/ij/ImageJ$ExceptionHandler.class b/bin/ij/ImageJ$ExceptionHandler.class new file mode 100644 index 0000000..9c8e523 Binary files /dev/null and b/bin/ij/ImageJ$ExceptionHandler.class differ diff --git a/bin/ij/ImageJ.class b/bin/ij/ImageJ.class new file mode 100644 index 0000000..4c5a84e Binary files /dev/null and b/bin/ij/ImageJ.class differ diff --git a/bin/ij/ImageJApplet.class b/bin/ij/ImageJApplet.class new file mode 100644 index 0000000..689d84d Binary files /dev/null and b/bin/ij/ImageJApplet.class differ diff --git a/bin/ij/ImageListener.class b/bin/ij/ImageListener.class new file mode 100644 index 0000000..b52b154 Binary files /dev/null and b/bin/ij/ImageListener.class differ diff --git a/bin/ij/ImagePlus$1.class b/bin/ij/ImagePlus$1.class new file mode 100644 index 0000000..2188236 Binary files /dev/null and b/bin/ij/ImagePlus$1.class differ diff --git a/bin/ij/ImagePlus.class b/bin/ij/ImagePlus.class new file mode 100644 index 0000000..6b93ba1 Binary files /dev/null and b/bin/ij/ImagePlus.class differ diff --git a/bin/ij/ImageStack.class b/bin/ij/ImageStack.class new file mode 100644 index 0000000..31a492d Binary files /dev/null and b/bin/ij/ImageStack.class differ diff --git a/bin/ij/LookUpTable.class b/bin/ij/LookUpTable.class new file mode 100644 index 0000000..c82d275 Binary files /dev/null and b/bin/ij/LookUpTable.class differ diff --git a/bin/ij/Macro.class b/bin/ij/Macro.class new file mode 100644 index 0000000..987fc9c Binary files /dev/null and b/bin/ij/Macro.class differ diff --git a/bin/ij/Menus.class b/bin/ij/Menus.class new file mode 100644 index 0000000..0bf6871 Binary files /dev/null and b/bin/ij/Menus.class differ diff --git a/bin/ij/OtherInstance$ImageJInstance.class b/bin/ij/OtherInstance$ImageJInstance.class new file mode 100644 index 0000000..7c4dc40 Binary files /dev/null and b/bin/ij/OtherInstance$ImageJInstance.class differ diff --git a/bin/ij/OtherInstance$Implementation.class b/bin/ij/OtherInstance$Implementation.class new file mode 100644 index 0000000..477550a Binary files /dev/null and b/bin/ij/OtherInstance$Implementation.class differ diff --git a/bin/ij/OtherInstance.class b/bin/ij/OtherInstance.class new file mode 100644 index 0000000..8936644 Binary files /dev/null and b/bin/ij/OtherInstance.class differ diff --git a/bin/ij/Prefs.class b/bin/ij/Prefs.class new file mode 100644 index 0000000..eef96f6 Binary files /dev/null and b/bin/ij/Prefs.class differ diff --git a/bin/ij/RecentOpener.class b/bin/ij/RecentOpener.class new file mode 100644 index 0000000..29ac499 Binary files /dev/null and b/bin/ij/RecentOpener.class differ diff --git a/bin/ij/Undo.class b/bin/ij/Undo.class new file mode 100644 index 0000000..976026c Binary files /dev/null and b/bin/ij/Undo.class differ diff --git a/bin/ij/VirtualStack.class b/bin/ij/VirtualStack.class new file mode 100644 index 0000000..3db5cbf Binary files /dev/null and b/bin/ij/VirtualStack.class differ diff --git a/bin/ij/WindowManager.class b/bin/ij/WindowManager.class new file mode 100644 index 0000000..0de8a34 Binary files /dev/null and b/bin/ij/WindowManager.class differ diff --git a/bin/ij/gui/Arrow.class b/bin/ij/gui/Arrow.class new file mode 100644 index 0000000..5de9d95 Binary files /dev/null and b/bin/ij/gui/Arrow.class differ diff --git a/bin/ij/gui/ColorChooser.class b/bin/ij/gui/ColorChooser.class new file mode 100644 index 0000000..ef77b98 Binary files /dev/null and b/bin/ij/gui/ColorChooser.class differ diff --git a/bin/ij/gui/ColorPanel.class b/bin/ij/gui/ColorPanel.class new file mode 100644 index 0000000..acc056a Binary files /dev/null and b/bin/ij/gui/ColorPanel.class differ diff --git a/bin/ij/gui/DialogListener.class b/bin/ij/gui/DialogListener.class new file mode 100644 index 0000000..88d4184 Binary files /dev/null and b/bin/ij/gui/DialogListener.class differ diff --git a/bin/ij/gui/EllipseRoi.class b/bin/ij/gui/EllipseRoi.class new file mode 100644 index 0000000..5b33cd2 Binary files /dev/null and b/bin/ij/gui/EllipseRoi.class differ diff --git a/bin/ij/gui/FreehandRoi.class b/bin/ij/gui/FreehandRoi.class new file mode 100644 index 0000000..4eff2c0 Binary files /dev/null and b/bin/ij/gui/FreehandRoi.class differ diff --git a/bin/ij/gui/GUI.class b/bin/ij/gui/GUI.class new file mode 100644 index 0000000..06a5e3a Binary files /dev/null and b/bin/ij/gui/GUI.class differ diff --git a/bin/ij/gui/GenericDialog$1.class b/bin/ij/gui/GenericDialog$1.class new file mode 100644 index 0000000..3247291 Binary files /dev/null and b/bin/ij/gui/GenericDialog$1.class differ diff --git a/bin/ij/gui/GenericDialog$BrowseButtonListener.class b/bin/ij/gui/GenericDialog$BrowseButtonListener.class new file mode 100644 index 0000000..1eb44eb Binary files /dev/null and b/bin/ij/gui/GenericDialog$BrowseButtonListener.class differ diff --git a/bin/ij/gui/GenericDialog$TextDropTarget.class b/bin/ij/gui/GenericDialog$TextDropTarget.class new file mode 100644 index 0000000..5acd288 Binary files /dev/null and b/bin/ij/gui/GenericDialog$TextDropTarget.class differ diff --git a/bin/ij/gui/GenericDialog.class b/bin/ij/gui/GenericDialog.class new file mode 100644 index 0000000..e077101 Binary files /dev/null and b/bin/ij/gui/GenericDialog.class differ diff --git a/bin/ij/gui/HTMLDialog$1.class b/bin/ij/gui/HTMLDialog$1.class new file mode 100644 index 0000000..aeaa502 Binary files /dev/null and b/bin/ij/gui/HTMLDialog$1.class differ diff --git a/bin/ij/gui/HTMLDialog$2.class b/bin/ij/gui/HTMLDialog$2.class new file mode 100644 index 0000000..48f55da Binary files /dev/null and b/bin/ij/gui/HTMLDialog$2.class differ diff --git a/bin/ij/gui/HTMLDialog.class b/bin/ij/gui/HTMLDialog.class new file mode 100644 index 0000000..522b7c7 Binary files /dev/null and b/bin/ij/gui/HTMLDialog.class differ diff --git a/bin/ij/gui/HistogramPlot.class b/bin/ij/gui/HistogramPlot.class new file mode 100644 index 0000000..9fabd0c Binary files /dev/null and b/bin/ij/gui/HistogramPlot.class differ diff --git a/bin/ij/gui/HistogramWindow.class b/bin/ij/gui/HistogramWindow.class new file mode 100644 index 0000000..e75813f Binary files /dev/null and b/bin/ij/gui/HistogramWindow.class differ diff --git a/bin/ij/gui/ImageCanvas$1.class b/bin/ij/gui/ImageCanvas$1.class new file mode 100644 index 0000000..8f7f08c Binary files /dev/null and b/bin/ij/gui/ImageCanvas$1.class differ diff --git a/bin/ij/gui/ImageCanvas.class b/bin/ij/gui/ImageCanvas.class new file mode 100644 index 0000000..44151c3 Binary files /dev/null and b/bin/ij/gui/ImageCanvas.class differ diff --git a/bin/ij/gui/ImageLayout.class b/bin/ij/gui/ImageLayout.class new file mode 100644 index 0000000..21485be Binary files /dev/null and b/bin/ij/gui/ImageLayout.class differ diff --git a/bin/ij/gui/ImagePanel.class b/bin/ij/gui/ImagePanel.class new file mode 100644 index 0000000..5516de2 Binary files /dev/null and b/bin/ij/gui/ImagePanel.class differ diff --git a/bin/ij/gui/ImageRoi.class b/bin/ij/gui/ImageRoi.class new file mode 100644 index 0000000..1395748 Binary files /dev/null and b/bin/ij/gui/ImageRoi.class differ diff --git a/bin/ij/gui/ImageWindow.class b/bin/ij/gui/ImageWindow.class new file mode 100644 index 0000000..cf14557 Binary files /dev/null and b/bin/ij/gui/ImageWindow.class differ diff --git a/bin/ij/gui/Line$PointIterator.class b/bin/ij/gui/Line$PointIterator.class new file mode 100644 index 0000000..576505c Binary files /dev/null and b/bin/ij/gui/Line$PointIterator.class differ diff --git a/bin/ij/gui/Line.class b/bin/ij/gui/Line.class new file mode 100644 index 0000000..ef6f264 Binary files /dev/null and b/bin/ij/gui/Line.class differ diff --git a/bin/ij/gui/MessageDialog.class b/bin/ij/gui/MessageDialog.class new file mode 100644 index 0000000..f5bdc43 Binary files /dev/null and b/bin/ij/gui/MessageDialog.class differ diff --git a/bin/ij/gui/MultiLineLabel.class b/bin/ij/gui/MultiLineLabel.class new file mode 100644 index 0000000..f24920a Binary files /dev/null and b/bin/ij/gui/MultiLineLabel.class differ diff --git a/bin/ij/gui/NewImage.class b/bin/ij/gui/NewImage.class new file mode 100644 index 0000000..ffa90d1 Binary files /dev/null and b/bin/ij/gui/NewImage.class differ diff --git a/bin/ij/gui/NonBlockingGenericDialog$1.class b/bin/ij/gui/NonBlockingGenericDialog$1.class new file mode 100644 index 0000000..2b7d6c1 Binary files /dev/null and b/bin/ij/gui/NonBlockingGenericDialog$1.class differ diff --git a/bin/ij/gui/NonBlockingGenericDialog$2.class b/bin/ij/gui/NonBlockingGenericDialog$2.class new file mode 100644 index 0000000..0706442 Binary files /dev/null and b/bin/ij/gui/NonBlockingGenericDialog$2.class differ diff --git a/bin/ij/gui/NonBlockingGenericDialog.class b/bin/ij/gui/NonBlockingGenericDialog.class new file mode 100644 index 0000000..97bea99 Binary files /dev/null and b/bin/ij/gui/NonBlockingGenericDialog.class differ diff --git a/bin/ij/gui/OvalRoi.class b/bin/ij/gui/OvalRoi.class new file mode 100644 index 0000000..aba14bb Binary files /dev/null and b/bin/ij/gui/OvalRoi.class differ diff --git a/bin/ij/gui/Overlay$1.class b/bin/ij/gui/Overlay$1.class new file mode 100644 index 0000000..907d4a6 Binary files /dev/null and b/bin/ij/gui/Overlay$1.class differ diff --git a/bin/ij/gui/Overlay.class b/bin/ij/gui/Overlay.class new file mode 100644 index 0000000..833e75c Binary files /dev/null and b/bin/ij/gui/Overlay.class differ diff --git a/bin/ij/gui/Plot.class b/bin/ij/gui/Plot.class new file mode 100644 index 0000000..58c834d Binary files /dev/null and b/bin/ij/gui/Plot.class differ diff --git a/bin/ij/gui/PlotCanvas.class b/bin/ij/gui/PlotCanvas.class new file mode 100644 index 0000000..58b6a79 Binary files /dev/null and b/bin/ij/gui/PlotCanvas.class differ diff --git a/bin/ij/gui/PlotContentsDialog.class b/bin/ij/gui/PlotContentsDialog.class new file mode 100644 index 0000000..c1aed04 Binary files /dev/null and b/bin/ij/gui/PlotContentsDialog.class differ diff --git a/bin/ij/gui/PlotDialog$1.class b/bin/ij/gui/PlotDialog$1.class new file mode 100644 index 0000000..adfa206 Binary files /dev/null and b/bin/ij/gui/PlotDialog$1.class differ diff --git a/bin/ij/gui/PlotDialog.class b/bin/ij/gui/PlotDialog.class new file mode 100644 index 0000000..afc1c2a Binary files /dev/null and b/bin/ij/gui/PlotDialog.class differ diff --git a/bin/ij/gui/PlotMaker.class b/bin/ij/gui/PlotMaker.class new file mode 100644 index 0000000..bd34bf9 Binary files /dev/null and b/bin/ij/gui/PlotMaker.class differ diff --git a/bin/ij/gui/PlotObject.class b/bin/ij/gui/PlotObject.class new file mode 100644 index 0000000..a9beee6 Binary files /dev/null and b/bin/ij/gui/PlotObject.class differ diff --git a/bin/ij/gui/PlotProperties.class b/bin/ij/gui/PlotProperties.class new file mode 100644 index 0000000..37eb6ce Binary files /dev/null and b/bin/ij/gui/PlotProperties.class differ diff --git a/bin/ij/gui/PlotVirtualStack.class b/bin/ij/gui/PlotVirtualStack.class new file mode 100644 index 0000000..5fc1af8 Binary files /dev/null and b/bin/ij/gui/PlotVirtualStack.class differ diff --git a/bin/ij/gui/PlotWindow$1.class b/bin/ij/gui/PlotWindow$1.class new file mode 100644 index 0000000..0369222 Binary files /dev/null and b/bin/ij/gui/PlotWindow$1.class differ diff --git a/bin/ij/gui/PlotWindow.class b/bin/ij/gui/PlotWindow.class new file mode 100644 index 0000000..24abb4e Binary files /dev/null and b/bin/ij/gui/PlotWindow.class differ diff --git a/bin/ij/gui/PointRoi$1.class b/bin/ij/gui/PointRoi$1.class new file mode 100644 index 0000000..0e3749b Binary files /dev/null and b/bin/ij/gui/PointRoi$1.class differ diff --git a/bin/ij/gui/PointRoi.class b/bin/ij/gui/PointRoi.class new file mode 100644 index 0000000..2a8ae75 Binary files /dev/null and b/bin/ij/gui/PointRoi.class differ diff --git a/bin/ij/gui/PolygonRoi.class b/bin/ij/gui/PolygonRoi.class new file mode 100644 index 0000000..b99cf93 Binary files /dev/null and b/bin/ij/gui/PolygonRoi.class differ diff --git a/bin/ij/gui/ProfilePlot.class b/bin/ij/gui/ProfilePlot.class new file mode 100644 index 0000000..2d2c812 Binary files /dev/null and b/bin/ij/gui/ProfilePlot.class differ diff --git a/bin/ij/gui/ProgressBar.class b/bin/ij/gui/ProgressBar.class new file mode 100644 index 0000000..de3f2bf Binary files /dev/null and b/bin/ij/gui/ProgressBar.class differ diff --git a/bin/ij/gui/Roi$RoiPointsIteratorMask.class b/bin/ij/gui/Roi$RoiPointsIteratorMask.class new file mode 100644 index 0000000..b6b9b52 Binary files /dev/null and b/bin/ij/gui/Roi$RoiPointsIteratorMask.class differ diff --git a/bin/ij/gui/Roi.class b/bin/ij/gui/Roi.class new file mode 100644 index 0000000..892625a Binary files /dev/null and b/bin/ij/gui/Roi.class differ diff --git a/bin/ij/gui/RoiBrush.class b/bin/ij/gui/RoiBrush.class new file mode 100644 index 0000000..a347add Binary files /dev/null and b/bin/ij/gui/RoiBrush.class differ diff --git a/bin/ij/gui/RoiDefaultsDialog.class b/bin/ij/gui/RoiDefaultsDialog.class new file mode 100644 index 0000000..115a522 Binary files /dev/null and b/bin/ij/gui/RoiDefaultsDialog.class differ diff --git a/bin/ij/gui/RoiListener.class b/bin/ij/gui/RoiListener.class new file mode 100644 index 0000000..335aab5 Binary files /dev/null and b/bin/ij/gui/RoiListener.class differ diff --git a/bin/ij/gui/RoiProperties.class b/bin/ij/gui/RoiProperties.class new file mode 100644 index 0000000..6ef1514 Binary files /dev/null and b/bin/ij/gui/RoiProperties.class differ diff --git a/bin/ij/gui/RotatedRectRoi.class b/bin/ij/gui/RotatedRectRoi.class new file mode 100644 index 0000000..c94868c Binary files /dev/null and b/bin/ij/gui/RotatedRectRoi.class differ diff --git a/bin/ij/gui/SaveChangesDialog.class b/bin/ij/gui/SaveChangesDialog.class new file mode 100644 index 0000000..6178d30 Binary files /dev/null and b/bin/ij/gui/SaveChangesDialog.class differ diff --git a/bin/ij/gui/ScrollbarWithLabel$Icon.class b/bin/ij/gui/ScrollbarWithLabel$Icon.class new file mode 100644 index 0000000..a69b7a1 Binary files /dev/null and b/bin/ij/gui/ScrollbarWithLabel$Icon.class differ diff --git a/bin/ij/gui/ScrollbarWithLabel.class b/bin/ij/gui/ScrollbarWithLabel.class new file mode 100644 index 0000000..980c1b8 Binary files /dev/null and b/bin/ij/gui/ScrollbarWithLabel.class differ diff --git a/bin/ij/gui/ShapeRoi.class b/bin/ij/gui/ShapeRoi.class new file mode 100644 index 0000000..4f584b7 Binary files /dev/null and b/bin/ij/gui/ShapeRoi.class differ diff --git a/bin/ij/gui/StackWindow$1.class b/bin/ij/gui/StackWindow$1.class new file mode 100644 index 0000000..85ada38 Binary files /dev/null and b/bin/ij/gui/StackWindow$1.class differ diff --git a/bin/ij/gui/StackWindow$2.class b/bin/ij/gui/StackWindow$2.class new file mode 100644 index 0000000..3bd8870 Binary files /dev/null and b/bin/ij/gui/StackWindow$2.class differ diff --git a/bin/ij/gui/StackWindow.class b/bin/ij/gui/StackWindow.class new file mode 100644 index 0000000..fdfcabe Binary files /dev/null and b/bin/ij/gui/StackWindow.class differ diff --git a/bin/ij/gui/TextRoi.class b/bin/ij/gui/TextRoi.class new file mode 100644 index 0000000..d779cbb Binary files /dev/null and b/bin/ij/gui/TextRoi.class differ diff --git a/bin/ij/gui/Toolbar$1.class b/bin/ij/gui/Toolbar$1.class new file mode 100644 index 0000000..3db2893 Binary files /dev/null and b/bin/ij/gui/Toolbar$1.class differ diff --git a/bin/ij/gui/Toolbar.class b/bin/ij/gui/Toolbar.class new file mode 100644 index 0000000..8f7d9cc Binary files /dev/null and b/bin/ij/gui/Toolbar.class differ diff --git a/bin/ij/gui/TrimmedButton.class b/bin/ij/gui/TrimmedButton.class new file mode 100644 index 0000000..beb580f Binary files /dev/null and b/bin/ij/gui/TrimmedButton.class differ diff --git a/bin/ij/gui/WaitForUserDialog.class b/bin/ij/gui/WaitForUserDialog.class new file mode 100644 index 0000000..040ada7 Binary files /dev/null and b/bin/ij/gui/WaitForUserDialog.class differ diff --git a/bin/ij/gui/Wand.class b/bin/ij/gui/Wand.class new file mode 100644 index 0000000..2d20f47 Binary files /dev/null and b/bin/ij/gui/Wand.class differ diff --git a/bin/ij/gui/YesNoCancelDialog.class b/bin/ij/gui/YesNoCancelDialog.class new file mode 100644 index 0000000..669b233 Binary files /dev/null and b/bin/ij/gui/YesNoCancelDialog.class differ diff --git a/bin/ij/io/BitBuffer.class b/bin/ij/io/BitBuffer.class new file mode 100644 index 0000000..95785bb Binary files /dev/null and b/bin/ij/io/BitBuffer.class differ diff --git a/bin/ij/io/ByteVector.class b/bin/ij/io/ByteVector.class new file mode 100644 index 0000000..023def5 Binary files /dev/null and b/bin/ij/io/ByteVector.class differ diff --git a/bin/ij/io/DirectoryChooser$1.class b/bin/ij/io/DirectoryChooser$1.class new file mode 100644 index 0000000..39d652d Binary files /dev/null and b/bin/ij/io/DirectoryChooser$1.class differ diff --git a/bin/ij/io/DirectoryChooser.class b/bin/ij/io/DirectoryChooser.class new file mode 100644 index 0000000..86bcb2b Binary files /dev/null and b/bin/ij/io/DirectoryChooser.class differ diff --git a/bin/ij/io/DragAndDropHandler.class b/bin/ij/io/DragAndDropHandler.class new file mode 100644 index 0000000..d87ce8b Binary files /dev/null and b/bin/ij/io/DragAndDropHandler.class differ diff --git a/bin/ij/io/FileInfo.class b/bin/ij/io/FileInfo.class new file mode 100644 index 0000000..4b40373 Binary files /dev/null and b/bin/ij/io/FileInfo.class differ diff --git a/bin/ij/io/FileOpener.class b/bin/ij/io/FileOpener.class new file mode 100644 index 0000000..037d854 Binary files /dev/null and b/bin/ij/io/FileOpener.class differ diff --git a/bin/ij/io/FileSaver.class b/bin/ij/io/FileSaver.class new file mode 100644 index 0000000..8093b35 Binary files /dev/null and b/bin/ij/io/FileSaver.class differ diff --git a/bin/ij/io/ImageReader.class b/bin/ij/io/ImageReader.class new file mode 100644 index 0000000..bf3c930 Binary files /dev/null and b/bin/ij/io/ImageReader.class differ diff --git a/bin/ij/io/ImageWriter.class b/bin/ij/io/ImageWriter.class new file mode 100644 index 0000000..4778a7e Binary files /dev/null and b/bin/ij/io/ImageWriter.class differ diff --git a/bin/ij/io/ImportDialog.class b/bin/ij/io/ImportDialog.class new file mode 100644 index 0000000..a02dbf0 Binary files /dev/null and b/bin/ij/io/ImportDialog.class differ diff --git a/bin/ij/io/LogStream.class b/bin/ij/io/LogStream.class new file mode 100644 index 0000000..5004228 Binary files /dev/null and b/bin/ij/io/LogStream.class differ diff --git a/bin/ij/io/OpenDialog$1.class b/bin/ij/io/OpenDialog$1.class new file mode 100644 index 0000000..51f6901 Binary files /dev/null and b/bin/ij/io/OpenDialog$1.class differ diff --git a/bin/ij/io/OpenDialog.class b/bin/ij/io/OpenDialog.class new file mode 100644 index 0000000..e427501 Binary files /dev/null and b/bin/ij/io/OpenDialog.class differ diff --git a/bin/ij/io/Opener$1.class b/bin/ij/io/Opener$1.class new file mode 100644 index 0000000..0e975ca Binary files /dev/null and b/bin/ij/io/Opener$1.class differ diff --git a/bin/ij/io/Opener.class b/bin/ij/io/Opener.class new file mode 100644 index 0000000..c4492eb Binary files /dev/null and b/bin/ij/io/Opener.class differ diff --git a/bin/ij/io/PluginClassLoader.class b/bin/ij/io/PluginClassLoader.class new file mode 100644 index 0000000..01f372a Binary files /dev/null and b/bin/ij/io/PluginClassLoader.class differ diff --git a/bin/ij/io/RandomAccessStream.class b/bin/ij/io/RandomAccessStream.class new file mode 100644 index 0000000..8668ff9 Binary files /dev/null and b/bin/ij/io/RandomAccessStream.class differ diff --git a/bin/ij/io/RoiDecoder.class b/bin/ij/io/RoiDecoder.class new file mode 100644 index 0000000..0898e90 Binary files /dev/null and b/bin/ij/io/RoiDecoder.class differ diff --git a/bin/ij/io/RoiEncoder.class b/bin/ij/io/RoiEncoder.class new file mode 100644 index 0000000..535883c Binary files /dev/null and b/bin/ij/io/RoiEncoder.class differ diff --git a/bin/ij/io/SaveDialog$1.class b/bin/ij/io/SaveDialog$1.class new file mode 100644 index 0000000..5cc7e18 Binary files /dev/null and b/bin/ij/io/SaveDialog$1.class differ diff --git a/bin/ij/io/SaveDialog.class b/bin/ij/io/SaveDialog.class new file mode 100644 index 0000000..3d51d86 Binary files /dev/null and b/bin/ij/io/SaveDialog.class differ diff --git a/bin/ij/io/TextEncoder.class b/bin/ij/io/TextEncoder.class new file mode 100644 index 0000000..caaffae Binary files /dev/null and b/bin/ij/io/TextEncoder.class differ diff --git a/bin/ij/io/TiffDecoder.class b/bin/ij/io/TiffDecoder.class new file mode 100644 index 0000000..2162547 Binary files /dev/null and b/bin/ij/io/TiffDecoder.class differ diff --git a/bin/ij/io/TiffEncoder.class b/bin/ij/io/TiffEncoder.class new file mode 100644 index 0000000..8552e57 Binary files /dev/null and b/bin/ij/io/TiffEncoder.class differ diff --git a/bin/ij/macro/Debugger.class b/bin/ij/macro/Debugger.class new file mode 100644 index 0000000..3a50082 Binary files /dev/null and b/bin/ij/macro/Debugger.class differ diff --git a/bin/ij/macro/ExtensionDescriptor.class b/bin/ij/macro/ExtensionDescriptor.class new file mode 100644 index 0000000..2e1a37e Binary files /dev/null and b/bin/ij/macro/ExtensionDescriptor.class differ diff --git a/bin/ij/macro/FunctionFinder.class b/bin/ij/macro/FunctionFinder.class new file mode 100644 index 0000000..c269bc7 Binary files /dev/null and b/bin/ij/macro/FunctionFinder.class differ diff --git a/bin/ij/macro/Functions.class b/bin/ij/macro/Functions.class new file mode 100644 index 0000000..3f6fdce Binary files /dev/null and b/bin/ij/macro/Functions.class differ diff --git a/bin/ij/macro/Interpreter.class b/bin/ij/macro/Interpreter.class new file mode 100644 index 0000000..0984591 Binary files /dev/null and b/bin/ij/macro/Interpreter.class differ diff --git a/bin/ij/macro/MacroConstants.class b/bin/ij/macro/MacroConstants.class new file mode 100644 index 0000000..ad4a52c Binary files /dev/null and b/bin/ij/macro/MacroConstants.class differ diff --git a/bin/ij/macro/MacroException.class b/bin/ij/macro/MacroException.class new file mode 100644 index 0000000..f9ce812 Binary files /dev/null and b/bin/ij/macro/MacroException.class differ diff --git a/bin/ij/macro/MacroExtension.class b/bin/ij/macro/MacroExtension.class new file mode 100644 index 0000000..e8ce470 Binary files /dev/null and b/bin/ij/macro/MacroExtension.class differ diff --git a/bin/ij/macro/MacroRunner.class b/bin/ij/macro/MacroRunner.class new file mode 100644 index 0000000..0a043e1 Binary files /dev/null and b/bin/ij/macro/MacroRunner.class differ diff --git a/bin/ij/macro/Program.class b/bin/ij/macro/Program.class new file mode 100644 index 0000000..6ab6073 Binary files /dev/null and b/bin/ij/macro/Program.class differ diff --git a/bin/ij/macro/ReturnException.class b/bin/ij/macro/ReturnException.class new file mode 100644 index 0000000..4c1217b Binary files /dev/null and b/bin/ij/macro/ReturnException.class differ diff --git a/bin/ij/macro/StartupRunner.class b/bin/ij/macro/StartupRunner.class new file mode 100644 index 0000000..50d7704 Binary files /dev/null and b/bin/ij/macro/StartupRunner.class differ diff --git a/bin/ij/macro/Symbol.class b/bin/ij/macro/Symbol.class new file mode 100644 index 0000000..c80cb48 Binary files /dev/null and b/bin/ij/macro/Symbol.class differ diff --git a/bin/ij/macro/Tokenizer.class b/bin/ij/macro/Tokenizer.class new file mode 100644 index 0000000..f913478 Binary files /dev/null and b/bin/ij/macro/Tokenizer.class differ diff --git a/bin/ij/macro/Variable.class b/bin/ij/macro/Variable.class new file mode 100644 index 0000000..8167ade Binary files /dev/null and b/bin/ij/macro/Variable.class differ diff --git a/bin/ij/measure/Calibration.class b/bin/ij/measure/Calibration.class new file mode 100644 index 0000000..4cf638b Binary files /dev/null and b/bin/ij/measure/Calibration.class differ diff --git a/bin/ij/measure/CurveFitter.class b/bin/ij/measure/CurveFitter.class new file mode 100644 index 0000000..0c734ba Binary files /dev/null and b/bin/ij/measure/CurveFitter.class differ diff --git a/bin/ij/measure/Measurements.class b/bin/ij/measure/Measurements.class new file mode 100644 index 0000000..c5b3456 Binary files /dev/null and b/bin/ij/measure/Measurements.class differ diff --git a/bin/ij/measure/Minimizer$1.class b/bin/ij/measure/Minimizer$1.class new file mode 100644 index 0000000..060832f Binary files /dev/null and b/bin/ij/measure/Minimizer$1.class differ diff --git a/bin/ij/measure/Minimizer.class b/bin/ij/measure/Minimizer.class new file mode 100644 index 0000000..902b204 Binary files /dev/null and b/bin/ij/measure/Minimizer.class differ diff --git a/bin/ij/measure/ResultsTable$ComparableEntry.class b/bin/ij/measure/ResultsTable$ComparableEntry.class new file mode 100644 index 0000000..4fb4bbd Binary files /dev/null and b/bin/ij/measure/ResultsTable$ComparableEntry.class differ diff --git a/bin/ij/measure/ResultsTable.class b/bin/ij/measure/ResultsTable.class new file mode 100644 index 0000000..5e28f3a Binary files /dev/null and b/bin/ij/measure/ResultsTable.class differ diff --git a/bin/ij/measure/ResultsTableMacros$1.class b/bin/ij/measure/ResultsTableMacros$1.class new file mode 100644 index 0000000..846a731 Binary files /dev/null and b/bin/ij/measure/ResultsTableMacros$1.class differ diff --git a/bin/ij/measure/ResultsTableMacros.class b/bin/ij/measure/ResultsTableMacros.class new file mode 100644 index 0000000..bc61858 Binary files /dev/null and b/bin/ij/measure/ResultsTableMacros.class differ diff --git a/bin/ij/measure/SplineFitter.class b/bin/ij/measure/SplineFitter.class new file mode 100644 index 0000000..bb11c5e Binary files /dev/null and b/bin/ij/measure/SplineFitter.class differ diff --git a/bin/ij/measure/UserFunction.class b/bin/ij/measure/UserFunction.class new file mode 100644 index 0000000..cbf2823 Binary files /dev/null and b/bin/ij/measure/UserFunction.class differ diff --git a/bin/ij/plugin/AVI_Reader$raInputStream.class b/bin/ij/plugin/AVI_Reader$raInputStream.class new file mode 100644 index 0000000..5248710 Binary files /dev/null and b/bin/ij/plugin/AVI_Reader$raInputStream.class differ diff --git a/bin/ij/plugin/AVI_Reader.class b/bin/ij/plugin/AVI_Reader.class new file mode 100644 index 0000000..d6c9d48 Binary files /dev/null and b/bin/ij/plugin/AVI_Reader.class differ diff --git a/bin/ij/plugin/AboutBox.class b/bin/ij/plugin/AboutBox.class new file mode 100644 index 0000000..549bfcb Binary files /dev/null and b/bin/ij/plugin/AboutBox.class differ diff --git a/bin/ij/plugin/AnimatedGifEncoder2.class b/bin/ij/plugin/AnimatedGifEncoder2.class new file mode 100644 index 0000000..88f74a6 Binary files /dev/null and b/bin/ij/plugin/AnimatedGifEncoder2.class differ diff --git a/bin/ij/plugin/Animator.class b/bin/ij/plugin/Animator.class new file mode 100644 index 0000000..0d9fbf2 Binary files /dev/null and b/bin/ij/plugin/Animator.class differ diff --git a/bin/ij/plugin/AppearanceOptions.class b/bin/ij/plugin/AppearanceOptions.class new file mode 100644 index 0000000..4acbf8c Binary files /dev/null and b/bin/ij/plugin/AppearanceOptions.class differ diff --git a/bin/ij/plugin/ArrowToolOptions.class b/bin/ij/plugin/ArrowToolOptions.class new file mode 100644 index 0000000..4d8116b Binary files /dev/null and b/bin/ij/plugin/ArrowToolOptions.class differ diff --git a/bin/ij/plugin/BMPDecoder.class b/bin/ij/plugin/BMPDecoder.class new file mode 100644 index 0000000..8305180 Binary files /dev/null and b/bin/ij/plugin/BMPDecoder.class differ diff --git a/bin/ij/plugin/BMP_Reader.class b/bin/ij/plugin/BMP_Reader.class new file mode 100644 index 0000000..45ca76b Binary files /dev/null and b/bin/ij/plugin/BMP_Reader.class differ diff --git a/bin/ij/plugin/BMP_Writer.class b/bin/ij/plugin/BMP_Writer.class new file mode 100644 index 0000000..fbac225 Binary files /dev/null and b/bin/ij/plugin/BMP_Writer.class differ diff --git a/bin/ij/plugin/BatchConverter.class b/bin/ij/plugin/BatchConverter.class new file mode 100644 index 0000000..739afe3 Binary files /dev/null and b/bin/ij/plugin/BatchConverter.class differ diff --git a/bin/ij/plugin/BatchMeasure.class b/bin/ij/plugin/BatchMeasure.class new file mode 100644 index 0000000..c8a789d Binary files /dev/null and b/bin/ij/plugin/BatchMeasure.class differ diff --git a/bin/ij/plugin/BatchProcessor.class b/bin/ij/plugin/BatchProcessor.class new file mode 100644 index 0000000..58d524b Binary files /dev/null and b/bin/ij/plugin/BatchProcessor.class differ diff --git a/bin/ij/plugin/Benchmark.class b/bin/ij/plugin/Benchmark.class new file mode 100644 index 0000000..6021568 Binary files /dev/null and b/bin/ij/plugin/Benchmark.class differ diff --git a/bin/ij/plugin/Binner.class b/bin/ij/plugin/Binner.class new file mode 100644 index 0000000..9b8f2f5 Binary files /dev/null and b/bin/ij/plugin/Binner.class differ diff --git a/bin/ij/plugin/BrowserLauncher.class b/bin/ij/plugin/BrowserLauncher.class new file mode 100644 index 0000000..6d64dce Binary files /dev/null and b/bin/ij/plugin/BrowserLauncher.class differ diff --git a/bin/ij/plugin/CalibrationBar$LiveDialog.class b/bin/ij/plugin/CalibrationBar$LiveDialog.class new file mode 100644 index 0000000..6c6ce20 Binary files /dev/null and b/bin/ij/plugin/CalibrationBar$LiveDialog.class differ diff --git a/bin/ij/plugin/CalibrationBar.class b/bin/ij/plugin/CalibrationBar.class new file mode 100644 index 0000000..b24d12f Binary files /dev/null and b/bin/ij/plugin/CalibrationBar.class differ diff --git a/bin/ij/plugin/CanvasResizer.class b/bin/ij/plugin/CanvasResizer.class new file mode 100644 index 0000000..0fe5242 Binary files /dev/null and b/bin/ij/plugin/CanvasResizer.class differ diff --git a/bin/ij/plugin/ChannelArranger.class b/bin/ij/plugin/ChannelArranger.class new file mode 100644 index 0000000..e2363e9 Binary files /dev/null and b/bin/ij/plugin/ChannelArranger.class differ diff --git a/bin/ij/plugin/ChannelSplitter.class b/bin/ij/plugin/ChannelSplitter.class new file mode 100644 index 0000000..ca63545 Binary files /dev/null and b/bin/ij/plugin/ChannelSplitter.class differ diff --git a/bin/ij/plugin/CircularRoiMaker.class b/bin/ij/plugin/CircularRoiMaker.class new file mode 100644 index 0000000..b9d2f84 Binary files /dev/null and b/bin/ij/plugin/CircularRoiMaker.class differ diff --git a/bin/ij/plugin/ClassChecker.class b/bin/ij/plugin/ClassChecker.class new file mode 100644 index 0000000..e3472b0 Binary files /dev/null and b/bin/ij/plugin/ClassChecker.class differ diff --git a/bin/ij/plugin/Clipboard.class b/bin/ij/plugin/Clipboard.class new file mode 100644 index 0000000..c001413 Binary files /dev/null and b/bin/ij/plugin/Clipboard.class differ diff --git a/bin/ij/plugin/ColorPanel.class b/bin/ij/plugin/ColorPanel.class new file mode 100644 index 0000000..7effee3 Binary files /dev/null and b/bin/ij/plugin/ColorPanel.class differ diff --git a/bin/ij/plugin/Colors.class b/bin/ij/plugin/Colors.class new file mode 100644 index 0000000..1430c36 Binary files /dev/null and b/bin/ij/plugin/Colors.class differ diff --git a/bin/ij/plugin/CommandFinder$1.class b/bin/ij/plugin/CommandFinder$1.class new file mode 100644 index 0000000..a8d41b4 Binary files /dev/null and b/bin/ij/plugin/CommandFinder$1.class differ diff --git a/bin/ij/plugin/CommandFinder$2.class b/bin/ij/plugin/CommandFinder$2.class new file mode 100644 index 0000000..61980d5 Binary files /dev/null and b/bin/ij/plugin/CommandFinder$2.class differ diff --git a/bin/ij/plugin/CommandFinder$CommandAction.class b/bin/ij/plugin/CommandFinder$CommandAction.class new file mode 100644 index 0000000..b976ab1 Binary files /dev/null and b/bin/ij/plugin/CommandFinder$CommandAction.class differ diff --git a/bin/ij/plugin/CommandFinder$PromptDocumentListener.class b/bin/ij/plugin/CommandFinder$PromptDocumentListener.class new file mode 100644 index 0000000..510c0f2 Binary files /dev/null and b/bin/ij/plugin/CommandFinder$PromptDocumentListener.class differ diff --git a/bin/ij/plugin/CommandFinder$TableModel.class b/bin/ij/plugin/CommandFinder$TableModel.class new file mode 100644 index 0000000..11a3a8c Binary files /dev/null and b/bin/ij/plugin/CommandFinder$TableModel.class differ diff --git a/bin/ij/plugin/CommandFinder.class b/bin/ij/plugin/CommandFinder.class new file mode 100644 index 0000000..4b4667b Binary files /dev/null and b/bin/ij/plugin/CommandFinder.class differ diff --git a/bin/ij/plugin/CommandLister.class b/bin/ij/plugin/CommandLister.class new file mode 100644 index 0000000..dcf8ad5 Binary files /dev/null and b/bin/ij/plugin/CommandLister.class differ diff --git a/bin/ij/plugin/Commands.class b/bin/ij/plugin/Commands.class new file mode 100644 index 0000000..8877f11 Binary files /dev/null and b/bin/ij/plugin/Commands.class differ diff --git a/bin/ij/plugin/Compiler.class b/bin/ij/plugin/Compiler.class new file mode 100644 index 0000000..fd73f83 Binary files /dev/null and b/bin/ij/plugin/Compiler.class differ diff --git a/bin/ij/plugin/CompilerTool$JavaxCompilerTool.class b/bin/ij/plugin/CompilerTool$JavaxCompilerTool.class new file mode 100644 index 0000000..5cdbc90 Binary files /dev/null and b/bin/ij/plugin/CompilerTool$JavaxCompilerTool.class differ diff --git a/bin/ij/plugin/CompilerTool$LegacyCompilerTool.class b/bin/ij/plugin/CompilerTool$LegacyCompilerTool.class new file mode 100644 index 0000000..448c967 Binary files /dev/null and b/bin/ij/plugin/CompilerTool$LegacyCompilerTool.class differ diff --git a/bin/ij/plugin/CompilerTool.class b/bin/ij/plugin/CompilerTool.class new file mode 100644 index 0000000..0ecf25a Binary files /dev/null and b/bin/ij/plugin/CompilerTool.class differ diff --git a/bin/ij/plugin/CompositeConverter.class b/bin/ij/plugin/CompositeConverter.class new file mode 100644 index 0000000..8d70853 Binary files /dev/null and b/bin/ij/plugin/CompositeConverter.class differ diff --git a/bin/ij/plugin/Concatenator.class b/bin/ij/plugin/Concatenator.class new file mode 100644 index 0000000..9c36069 Binary files /dev/null and b/bin/ij/plugin/Concatenator.class differ diff --git a/bin/ij/plugin/ContrastEnhancer.class b/bin/ij/plugin/ContrastEnhancer.class new file mode 100644 index 0000000..783e764 Binary files /dev/null and b/bin/ij/plugin/ContrastEnhancer.class differ diff --git a/bin/ij/plugin/ControlPanel.class b/bin/ij/plugin/ControlPanel.class new file mode 100644 index 0000000..f0866db Binary files /dev/null and b/bin/ij/plugin/ControlPanel.class differ diff --git a/bin/ij/plugin/Converter.class b/bin/ij/plugin/Converter.class new file mode 100644 index 0000000..a126d68 Binary files /dev/null and b/bin/ij/plugin/Converter.class differ diff --git a/bin/ij/plugin/Coordinates.class b/bin/ij/plugin/Coordinates.class new file mode 100644 index 0000000..60ec9ac Binary files /dev/null and b/bin/ij/plugin/Coordinates.class differ diff --git a/bin/ij/plugin/DICOM.class b/bin/ij/plugin/DICOM.class new file mode 100644 index 0000000..dde5e38 Binary files /dev/null and b/bin/ij/plugin/DICOM.class differ diff --git a/bin/ij/plugin/DicomDecoder.class b/bin/ij/plugin/DicomDecoder.class new file mode 100644 index 0000000..b53558b Binary files /dev/null and b/bin/ij/plugin/DicomDecoder.class differ diff --git a/bin/ij/plugin/DicomDictionary.class b/bin/ij/plugin/DicomDictionary.class new file mode 100644 index 0000000..ad48b3c Binary files /dev/null and b/bin/ij/plugin/DicomDictionary.class differ diff --git a/bin/ij/plugin/Distribution.class b/bin/ij/plugin/Distribution.class new file mode 100644 index 0000000..4d0a3b0 Binary files /dev/null and b/bin/ij/plugin/Distribution.class differ diff --git a/bin/ij/plugin/DragAndDrop.class b/bin/ij/plugin/DragAndDrop.class new file mode 100644 index 0000000..856d110 Binary files /dev/null and b/bin/ij/plugin/DragAndDrop.class differ diff --git a/bin/ij/plugin/Duplicator.class b/bin/ij/plugin/Duplicator.class new file mode 100644 index 0000000..14f4dc3 Binary files /dev/null and b/bin/ij/plugin/Duplicator.class differ diff --git a/bin/ij/plugin/EventListener.class b/bin/ij/plugin/EventListener.class new file mode 100644 index 0000000..a839e08 Binary files /dev/null and b/bin/ij/plugin/EventListener.class differ diff --git a/bin/ij/plugin/FFT.class b/bin/ij/plugin/FFT.class new file mode 100644 index 0000000..30786c2 Binary files /dev/null and b/bin/ij/plugin/FFT.class differ diff --git a/bin/ij/plugin/FFTMath.class b/bin/ij/plugin/FFTMath.class new file mode 100644 index 0000000..0007d6a Binary files /dev/null and b/bin/ij/plugin/FFTMath.class differ diff --git a/bin/ij/plugin/FITS_Reader.class b/bin/ij/plugin/FITS_Reader.class new file mode 100644 index 0000000..d392704 Binary files /dev/null and b/bin/ij/plugin/FITS_Reader.class differ diff --git a/bin/ij/plugin/FITS_Writer.class b/bin/ij/plugin/FITS_Writer.class new file mode 100644 index 0000000..8ef320f Binary files /dev/null and b/bin/ij/plugin/FITS_Writer.class differ diff --git a/bin/ij/plugin/FileInfoVirtualStack.class b/bin/ij/plugin/FileInfoVirtualStack.class new file mode 100644 index 0000000..ebb75a5 Binary files /dev/null and b/bin/ij/plugin/FileInfoVirtualStack.class differ diff --git a/bin/ij/plugin/Filters3D$1.class b/bin/ij/plugin/Filters3D$1.class new file mode 100644 index 0000000..8c286e6 Binary files /dev/null and b/bin/ij/plugin/Filters3D$1.class differ diff --git a/bin/ij/plugin/Filters3D.class b/bin/ij/plugin/Filters3D.class new file mode 100644 index 0000000..0ac13fb Binary files /dev/null and b/bin/ij/plugin/Filters3D.class differ diff --git a/bin/ij/plugin/FitsDecoder.class b/bin/ij/plugin/FitsDecoder.class new file mode 100644 index 0000000..a721bba Binary files /dev/null and b/bin/ij/plugin/FitsDecoder.class differ diff --git a/bin/ij/plugin/FolderOpener.class b/bin/ij/plugin/FolderOpener.class new file mode 100644 index 0000000..36bf4e0 Binary files /dev/null and b/bin/ij/plugin/FolderOpener.class differ diff --git a/bin/ij/plugin/GIF_Reader.class b/bin/ij/plugin/GIF_Reader.class new file mode 100644 index 0000000..e22a930 Binary files /dev/null and b/bin/ij/plugin/GIF_Reader.class differ diff --git a/bin/ij/plugin/GaussianBlur3D.class b/bin/ij/plugin/GaussianBlur3D.class new file mode 100644 index 0000000..35562d2 Binary files /dev/null and b/bin/ij/plugin/GaussianBlur3D.class differ diff --git a/bin/ij/plugin/GelAnalyzer.class b/bin/ij/plugin/GelAnalyzer.class new file mode 100644 index 0000000..5aa2e72 Binary files /dev/null and b/bin/ij/plugin/GelAnalyzer.class differ diff --git a/bin/ij/plugin/GifDecoder.class b/bin/ij/plugin/GifDecoder.class new file mode 100644 index 0000000..e29d6ef Binary files /dev/null and b/bin/ij/plugin/GifDecoder.class differ diff --git a/bin/ij/plugin/GifFrame.class b/bin/ij/plugin/GifFrame.class new file mode 100644 index 0000000..be72129 Binary files /dev/null and b/bin/ij/plugin/GifFrame.class differ diff --git a/bin/ij/plugin/GifWriter.class b/bin/ij/plugin/GifWriter.class new file mode 100644 index 0000000..5a9aac8 Binary files /dev/null and b/bin/ij/plugin/GifWriter.class differ diff --git a/bin/ij/plugin/Grid.class b/bin/ij/plugin/Grid.class new file mode 100644 index 0000000..bb1b75f Binary files /dev/null and b/bin/ij/plugin/Grid.class differ diff --git a/bin/ij/plugin/GroupedZProjector.class b/bin/ij/plugin/GroupedZProjector.class new file mode 100644 index 0000000..abc8457 Binary files /dev/null and b/bin/ij/plugin/GroupedZProjector.class differ diff --git a/bin/ij/plugin/Histogram.class b/bin/ij/plugin/Histogram.class new file mode 100644 index 0000000..fc5d21a Binary files /dev/null and b/bin/ij/plugin/Histogram.class differ diff --git a/bin/ij/plugin/Hotkeys.class b/bin/ij/plugin/Hotkeys.class new file mode 100644 index 0000000..6a2b119 Binary files /dev/null and b/bin/ij/plugin/Hotkeys.class differ diff --git a/bin/ij/plugin/HyperStackConverter.class b/bin/ij/plugin/HyperStackConverter.class new file mode 100644 index 0000000..cc00a79 Binary files /dev/null and b/bin/ij/plugin/HyperStackConverter.class differ diff --git a/bin/ij/plugin/HyperStackMaker.class b/bin/ij/plugin/HyperStackMaker.class new file mode 100644 index 0000000..b911ef4 Binary files /dev/null and b/bin/ij/plugin/HyperStackMaker.class differ diff --git a/bin/ij/plugin/HyperStackReducer.class b/bin/ij/plugin/HyperStackReducer.class new file mode 100644 index 0000000..8e865b9 Binary files /dev/null and b/bin/ij/plugin/HyperStackReducer.class differ diff --git a/bin/ij/plugin/ImageCalculator.class b/bin/ij/plugin/ImageCalculator.class new file mode 100644 index 0000000..a09a60f Binary files /dev/null and b/bin/ij/plugin/ImageCalculator.class differ diff --git a/bin/ij/plugin/ImageInfo.class b/bin/ij/plugin/ImageInfo.class new file mode 100644 index 0000000..0531163 Binary files /dev/null and b/bin/ij/plugin/ImageInfo.class differ diff --git a/bin/ij/plugin/ImageJ_Updater.class b/bin/ij/plugin/ImageJ_Updater.class new file mode 100644 index 0000000..710501e Binary files /dev/null and b/bin/ij/plugin/ImageJ_Updater.class differ diff --git a/bin/ij/plugin/ImagesToStack.class b/bin/ij/plugin/ImagesToStack.class new file mode 100644 index 0000000..027551b Binary files /dev/null and b/bin/ij/plugin/ImagesToStack.class differ diff --git a/bin/ij/plugin/JavaProperties.class b/bin/ij/plugin/JavaProperties.class new file mode 100644 index 0000000..464c318 Binary files /dev/null and b/bin/ij/plugin/JavaProperties.class differ diff --git a/bin/ij/plugin/JavaScriptEvaluator.class b/bin/ij/plugin/JavaScriptEvaluator.class new file mode 100644 index 0000000..ceffd21 Binary files /dev/null and b/bin/ij/plugin/JavaScriptEvaluator.class differ diff --git a/bin/ij/plugin/JpegWriter.class b/bin/ij/plugin/JpegWriter.class new file mode 100644 index 0000000..af0287b Binary files /dev/null and b/bin/ij/plugin/JpegWriter.class differ diff --git a/bin/ij/plugin/LUT_Editor.class b/bin/ij/plugin/LUT_Editor.class new file mode 100644 index 0000000..d8b2fd4 Binary files /dev/null and b/bin/ij/plugin/LUT_Editor.class differ diff --git a/bin/ij/plugin/LZWEncoder.class b/bin/ij/plugin/LZWEncoder.class new file mode 100644 index 0000000..5f923c3 Binary files /dev/null and b/bin/ij/plugin/LZWEncoder.class differ diff --git a/bin/ij/plugin/LZWEncoder2.class b/bin/ij/plugin/LZWEncoder2.class new file mode 100644 index 0000000..b14ee19 Binary files /dev/null and b/bin/ij/plugin/LZWEncoder2.class differ diff --git a/bin/ij/plugin/ListVirtualStack.class b/bin/ij/plugin/ListVirtualStack.class new file mode 100644 index 0000000..49f36b4 Binary files /dev/null and b/bin/ij/plugin/ListVirtualStack.class differ diff --git a/bin/ij/plugin/LutLoader.class b/bin/ij/plugin/LutLoader.class new file mode 100644 index 0000000..b51b224 Binary files /dev/null and b/bin/ij/plugin/LutLoader.class differ diff --git a/bin/ij/plugin/MacroInstaller.class b/bin/ij/plugin/MacroInstaller.class new file mode 100644 index 0000000..5d206b9 Binary files /dev/null and b/bin/ij/plugin/MacroInstaller.class differ diff --git a/bin/ij/plugin/Macro_Runner.class b/bin/ij/plugin/Macro_Runner.class new file mode 100644 index 0000000..482fd9a Binary files /dev/null and b/bin/ij/plugin/Macro_Runner.class differ diff --git a/bin/ij/plugin/MeasurementsWriter.class b/bin/ij/plugin/MeasurementsWriter.class new file mode 100644 index 0000000..e0eea1f Binary files /dev/null and b/bin/ij/plugin/MeasurementsWriter.class differ diff --git a/bin/ij/plugin/Memory.class b/bin/ij/plugin/Memory.class new file mode 100644 index 0000000..cea6090 Binary files /dev/null and b/bin/ij/plugin/Memory.class differ diff --git a/bin/ij/plugin/MontageMaker.class b/bin/ij/plugin/MontageMaker.class new file mode 100644 index 0000000..ae30471 Binary files /dev/null and b/bin/ij/plugin/MontageMaker.class differ diff --git a/bin/ij/plugin/NeuQuant.class b/bin/ij/plugin/NeuQuant.class new file mode 100644 index 0000000..e8ef458 Binary files /dev/null and b/bin/ij/plugin/NeuQuant.class differ diff --git a/bin/ij/plugin/NewPlugin.class b/bin/ij/plugin/NewPlugin.class new file mode 100644 index 0000000..eadfd18 Binary files /dev/null and b/bin/ij/plugin/NewPlugin.class differ diff --git a/bin/ij/plugin/NextImageOpener.class b/bin/ij/plugin/NextImageOpener.class new file mode 100644 index 0000000..da2df14 Binary files /dev/null and b/bin/ij/plugin/NextImageOpener.class differ diff --git a/bin/ij/plugin/Options.class b/bin/ij/plugin/Options.class new file mode 100644 index 0000000..d034165 Binary files /dev/null and b/bin/ij/plugin/Options.class differ diff --git a/bin/ij/plugin/Orthogonal_Views.class b/bin/ij/plugin/Orthogonal_Views.class new file mode 100644 index 0000000..696f4d7 Binary files /dev/null and b/bin/ij/plugin/Orthogonal_Views.class differ diff --git a/bin/ij/plugin/OverlayCommands.class b/bin/ij/plugin/OverlayCommands.class new file mode 100644 index 0000000..5c3bc14 Binary files /dev/null and b/bin/ij/plugin/OverlayCommands.class differ diff --git a/bin/ij/plugin/OverlayLabels.class b/bin/ij/plugin/OverlayLabels.class new file mode 100644 index 0000000..7e8828f Binary files /dev/null and b/bin/ij/plugin/OverlayLabels.class differ diff --git a/bin/ij/plugin/PGM_Reader.class b/bin/ij/plugin/PGM_Reader.class new file mode 100644 index 0000000..7eec95b Binary files /dev/null and b/bin/ij/plugin/PGM_Reader.class differ diff --git a/bin/ij/plugin/PNG_Writer.class b/bin/ij/plugin/PNG_Writer.class new file mode 100644 index 0000000..69b134a Binary files /dev/null and b/bin/ij/plugin/PNG_Writer.class differ diff --git a/bin/ij/plugin/PNM_Writer.class b/bin/ij/plugin/PNM_Writer.class new file mode 100644 index 0000000..a50f0b6 Binary files /dev/null and b/bin/ij/plugin/PNM_Writer.class differ diff --git a/bin/ij/plugin/Plots.class b/bin/ij/plugin/Plots.class new file mode 100644 index 0000000..5721110 Binary files /dev/null and b/bin/ij/plugin/Plots.class differ diff --git a/bin/ij/plugin/PlotsCanvas.class b/bin/ij/plugin/PlotsCanvas.class new file mode 100644 index 0000000..9ea7cac Binary files /dev/null and b/bin/ij/plugin/PlotsCanvas.class differ diff --git a/bin/ij/plugin/PlugIn.class b/bin/ij/plugin/PlugIn.class new file mode 100644 index 0000000..76dc09c Binary files /dev/null and b/bin/ij/plugin/PlugIn.class differ diff --git a/bin/ij/plugin/PlugInExecuter.class b/bin/ij/plugin/PlugInExecuter.class new file mode 100644 index 0000000..55acce3 Binary files /dev/null and b/bin/ij/plugin/PlugInExecuter.class differ diff --git a/bin/ij/plugin/PlugInInterpreter.class b/bin/ij/plugin/PlugInInterpreter.class new file mode 100644 index 0000000..d61cbf6 Binary files /dev/null and b/bin/ij/plugin/PlugInInterpreter.class differ diff --git a/bin/ij/plugin/PluginInstaller.class b/bin/ij/plugin/PluginInstaller.class new file mode 100644 index 0000000..fa18f97 Binary files /dev/null and b/bin/ij/plugin/PluginInstaller.class differ diff --git a/bin/ij/plugin/PointToolOptions.class b/bin/ij/plugin/PointToolOptions.class new file mode 100644 index 0000000..765b9a3 Binary files /dev/null and b/bin/ij/plugin/PointToolOptions.class differ diff --git a/bin/ij/plugin/Profiler.class b/bin/ij/plugin/Profiler.class new file mode 100644 index 0000000..2c9e90c Binary files /dev/null and b/bin/ij/plugin/Profiler.class differ diff --git a/bin/ij/plugin/Projector.class b/bin/ij/plugin/Projector.class new file mode 100644 index 0000000..b87c39a Binary files /dev/null and b/bin/ij/plugin/Projector.class differ diff --git a/bin/ij/plugin/ProxySettings.class b/bin/ij/plugin/ProxySettings.class new file mode 100644 index 0000000..a6d94f1 Binary files /dev/null and b/bin/ij/plugin/ProxySettings.class differ diff --git a/bin/ij/plugin/RGBStackConverter.class b/bin/ij/plugin/RGBStackConverter.class new file mode 100644 index 0000000..b974fd4 Binary files /dev/null and b/bin/ij/plugin/RGBStackConverter.class differ diff --git a/bin/ij/plugin/RGBStackMerge.class b/bin/ij/plugin/RGBStackMerge.class new file mode 100644 index 0000000..e5a3284 Binary files /dev/null and b/bin/ij/plugin/RGBStackMerge.class differ diff --git a/bin/ij/plugin/RandomOvals.txt b/bin/ij/plugin/RandomOvals.txt new file mode 100644 index 0000000..88c8548 --- /dev/null +++ b/bin/ij/plugin/RandomOvals.txt @@ -0,0 +1,15 @@ + + newImage("Untitled", "RGB", 400, 400, 1); + width = getWidth(); + height = getHeight(); + for (i=0; i<1000; i++) { + if (nSlices!=1) exit; + w = random()*width/2+1; + h = random()*width/2+1; + x = random()*width-w/2; + y = random()*height-h/2; + setForegroundColor(random()*255, random()*255, random()*255); + makeOval(x, y, w, h); + run("Fill"); + } + diff --git a/bin/ij/plugin/Raw.class b/bin/ij/plugin/Raw.class new file mode 100644 index 0000000..fc9279f Binary files /dev/null and b/bin/ij/plugin/Raw.class differ diff --git a/bin/ij/plugin/RectToolOptions.class b/bin/ij/plugin/RectToolOptions.class new file mode 100644 index 0000000..212c5b0 Binary files /dev/null and b/bin/ij/plugin/RectToolOptions.class differ diff --git a/bin/ij/plugin/Resizer.class b/bin/ij/plugin/Resizer.class new file mode 100644 index 0000000..cf7ed35 Binary files /dev/null and b/bin/ij/plugin/Resizer.class differ diff --git a/bin/ij/plugin/RoiEnlarger.class b/bin/ij/plugin/RoiEnlarger.class new file mode 100644 index 0000000..847113d Binary files /dev/null and b/bin/ij/plugin/RoiEnlarger.class differ diff --git a/bin/ij/plugin/RoiInterpolator.class b/bin/ij/plugin/RoiInterpolator.class new file mode 100644 index 0000000..9f5a4e5 Binary files /dev/null and b/bin/ij/plugin/RoiInterpolator.class differ diff --git a/bin/ij/plugin/RoiReader.class b/bin/ij/plugin/RoiReader.class new file mode 100644 index 0000000..699f84a Binary files /dev/null and b/bin/ij/plugin/RoiReader.class differ diff --git a/bin/ij/plugin/RoiRotator.class b/bin/ij/plugin/RoiRotator.class new file mode 100644 index 0000000..9c1760c Binary files /dev/null and b/bin/ij/plugin/RoiRotator.class differ diff --git a/bin/ij/plugin/RoiScaler.class b/bin/ij/plugin/RoiScaler.class new file mode 100644 index 0000000..c1c66c5 Binary files /dev/null and b/bin/ij/plugin/RoiScaler.class differ diff --git a/bin/ij/plugin/ScaleBar$BarDialog.class b/bin/ij/plugin/ScaleBar$BarDialog.class new file mode 100644 index 0000000..2ad7b4a Binary files /dev/null and b/bin/ij/plugin/ScaleBar$BarDialog.class differ diff --git a/bin/ij/plugin/ScaleBar$BarDialogListener.class b/bin/ij/plugin/ScaleBar$BarDialogListener.class new file mode 100644 index 0000000..da61114 Binary files /dev/null and b/bin/ij/plugin/ScaleBar$BarDialogListener.class differ diff --git a/bin/ij/plugin/ScaleBar$MissingRoiException.class b/bin/ij/plugin/ScaleBar$MissingRoiException.class new file mode 100644 index 0000000..e2bea27 Binary files /dev/null and b/bin/ij/plugin/ScaleBar$MissingRoiException.class differ diff --git a/bin/ij/plugin/ScaleBar$ScaleBarConfiguration.class b/bin/ij/plugin/ScaleBar$ScaleBarConfiguration.class new file mode 100644 index 0000000..827fc16 Binary files /dev/null and b/bin/ij/plugin/ScaleBar$ScaleBarConfiguration.class differ diff --git a/bin/ij/plugin/ScaleBar.class b/bin/ij/plugin/ScaleBar.class new file mode 100644 index 0000000..46ebbc4 Binary files /dev/null and b/bin/ij/plugin/ScaleBar.class differ diff --git a/bin/ij/plugin/Scaler.class b/bin/ij/plugin/Scaler.class new file mode 100644 index 0000000..ea177c8 Binary files /dev/null and b/bin/ij/plugin/Scaler.class differ diff --git a/bin/ij/plugin/ScreenGrabber.class b/bin/ij/plugin/ScreenGrabber.class new file mode 100644 index 0000000..6530e9a Binary files /dev/null and b/bin/ij/plugin/ScreenGrabber.class differ diff --git a/bin/ij/plugin/Selection.class b/bin/ij/plugin/Selection.class new file mode 100644 index 0000000..0935d7e Binary files /dev/null and b/bin/ij/plugin/Selection.class differ diff --git a/bin/ij/plugin/SimpleCommands$1.class b/bin/ij/plugin/SimpleCommands$1.class new file mode 100644 index 0000000..21719c0 Binary files /dev/null and b/bin/ij/plugin/SimpleCommands$1.class differ diff --git a/bin/ij/plugin/SimpleCommands.class b/bin/ij/plugin/SimpleCommands.class new file mode 100644 index 0000000..33d3087 Binary files /dev/null and b/bin/ij/plugin/SimpleCommands.class differ diff --git a/bin/ij/plugin/Slicer.class b/bin/ij/plugin/Slicer.class new file mode 100644 index 0000000..27ed10f Binary files /dev/null and b/bin/ij/plugin/Slicer.class differ diff --git a/bin/ij/plugin/SpecifyROI.class b/bin/ij/plugin/SpecifyROI.class new file mode 100644 index 0000000..2160793 Binary files /dev/null and b/bin/ij/plugin/SpecifyROI.class differ diff --git a/bin/ij/plugin/StackCombiner.class b/bin/ij/plugin/StackCombiner.class new file mode 100644 index 0000000..8481830 Binary files /dev/null and b/bin/ij/plugin/StackCombiner.class differ diff --git a/bin/ij/plugin/StackEditor.class b/bin/ij/plugin/StackEditor.class new file mode 100644 index 0000000..6371ab0 Binary files /dev/null and b/bin/ij/plugin/StackEditor.class differ diff --git a/bin/ij/plugin/StackInserter.class b/bin/ij/plugin/StackInserter.class new file mode 100644 index 0000000..b3f7048 Binary files /dev/null and b/bin/ij/plugin/StackInserter.class differ diff --git a/bin/ij/plugin/StackMaker.class b/bin/ij/plugin/StackMaker.class new file mode 100644 index 0000000..bab4cc9 Binary files /dev/null and b/bin/ij/plugin/StackMaker.class differ diff --git a/bin/ij/plugin/StackPlotter.class b/bin/ij/plugin/StackPlotter.class new file mode 100644 index 0000000..a4959e2 Binary files /dev/null and b/bin/ij/plugin/StackPlotter.class differ diff --git a/bin/ij/plugin/StackReducer.class b/bin/ij/plugin/StackReducer.class new file mode 100644 index 0000000..16694bd Binary files /dev/null and b/bin/ij/plugin/StackReducer.class differ diff --git a/bin/ij/plugin/StackReverser.class b/bin/ij/plugin/StackReverser.class new file mode 100644 index 0000000..8aca692 Binary files /dev/null and b/bin/ij/plugin/StackReverser.class differ diff --git a/bin/ij/plugin/StackWriter.class b/bin/ij/plugin/StackWriter.class new file mode 100644 index 0000000..6bcd9aa Binary files /dev/null and b/bin/ij/plugin/StackWriter.class differ diff --git a/bin/ij/plugin/Stack_Statistics.class b/bin/ij/plugin/Stack_Statistics.class new file mode 100644 index 0000000..8a3cbaf Binary files /dev/null and b/bin/ij/plugin/Stack_Statistics.class differ diff --git a/bin/ij/plugin/Startup.class b/bin/ij/plugin/Startup.class new file mode 100644 index 0000000..70796d9 Binary files /dev/null and b/bin/ij/plugin/Startup.class differ diff --git a/bin/ij/plugin/Straightener.class b/bin/ij/plugin/Straightener.class new file mode 100644 index 0000000..0e9d7e7 Binary files /dev/null and b/bin/ij/plugin/Straightener.class differ diff --git a/bin/ij/plugin/SubHyperstackMaker.class b/bin/ij/plugin/SubHyperstackMaker.class new file mode 100644 index 0000000..e096854 Binary files /dev/null and b/bin/ij/plugin/SubHyperstackMaker.class differ diff --git a/bin/ij/plugin/SubstackMaker.class b/bin/ij/plugin/SubstackMaker.class new file mode 100644 index 0000000..14df673 Binary files /dev/null and b/bin/ij/plugin/SubstackMaker.class differ diff --git a/bin/ij/plugin/SurfacePlotter.class b/bin/ij/plugin/SurfacePlotter.class new file mode 100644 index 0000000..6229087 Binary files /dev/null and b/bin/ij/plugin/SurfacePlotter.class differ diff --git a/bin/ij/plugin/Text.class b/bin/ij/plugin/Text.class new file mode 100644 index 0000000..898e5c9 Binary files /dev/null and b/bin/ij/plugin/Text.class differ diff --git a/bin/ij/plugin/TextFileReader.class b/bin/ij/plugin/TextFileReader.class new file mode 100644 index 0000000..5cf5504 Binary files /dev/null and b/bin/ij/plugin/TextFileReader.class differ diff --git a/bin/ij/plugin/TextReader.class b/bin/ij/plugin/TextReader.class new file mode 100644 index 0000000..56baf13 Binary files /dev/null and b/bin/ij/plugin/TextReader.class differ diff --git a/bin/ij/plugin/TextWriter.class b/bin/ij/plugin/TextWriter.class new file mode 100644 index 0000000..c4d50bd Binary files /dev/null and b/bin/ij/plugin/TextWriter.class differ diff --git a/bin/ij/plugin/ThreadLister$1.class b/bin/ij/plugin/ThreadLister$1.class new file mode 100644 index 0000000..fd5a9f3 Binary files /dev/null and b/bin/ij/plugin/ThreadLister$1.class differ diff --git a/bin/ij/plugin/ThreadLister.class b/bin/ij/plugin/ThreadLister.class new file mode 100644 index 0000000..a2941ae Binary files /dev/null and b/bin/ij/plugin/ThreadLister.class differ diff --git a/bin/ij/plugin/Thresholder.class b/bin/ij/plugin/Thresholder.class new file mode 100644 index 0000000..9e11052 Binary files /dev/null and b/bin/ij/plugin/Thresholder.class differ diff --git a/bin/ij/plugin/ThumbnailsCanvas.class b/bin/ij/plugin/ThumbnailsCanvas.class new file mode 100644 index 0000000..e4f7918 Binary files /dev/null and b/bin/ij/plugin/ThumbnailsCanvas.class differ diff --git a/bin/ij/plugin/TreePanel$1.class b/bin/ij/plugin/TreePanel$1.class new file mode 100644 index 0000000..14d6467 Binary files /dev/null and b/bin/ij/plugin/TreePanel$1.class differ diff --git a/bin/ij/plugin/TreePanel$2.class b/bin/ij/plugin/TreePanel$2.class new file mode 100644 index 0000000..5e43bdd Binary files /dev/null and b/bin/ij/plugin/TreePanel$2.class differ diff --git a/bin/ij/plugin/TreePanel$3.class b/bin/ij/plugin/TreePanel$3.class new file mode 100644 index 0000000..787b43a Binary files /dev/null and b/bin/ij/plugin/TreePanel$3.class differ diff --git a/bin/ij/plugin/TreePanel.class b/bin/ij/plugin/TreePanel.class new file mode 100644 index 0000000..a61c60e Binary files /dev/null and b/bin/ij/plugin/TreePanel.class differ diff --git a/bin/ij/plugin/URLOpener.class b/bin/ij/plugin/URLOpener.class new file mode 100644 index 0000000..f11225c Binary files /dev/null and b/bin/ij/plugin/URLOpener.class differ diff --git a/bin/ij/plugin/WandToolOptions.class b/bin/ij/plugin/WandToolOptions.class new file mode 100644 index 0000000..a40ad46 Binary files /dev/null and b/bin/ij/plugin/WandToolOptions.class differ diff --git a/bin/ij/plugin/WindowOrganizer.class b/bin/ij/plugin/WindowOrganizer.class new file mode 100644 index 0000000..7b072c1 Binary files /dev/null and b/bin/ij/plugin/WindowOrganizer.class differ diff --git a/bin/ij/plugin/XYCoordinates.class b/bin/ij/plugin/XYCoordinates.class new file mode 100644 index 0000000..1141dc6 Binary files /dev/null and b/bin/ij/plugin/XYCoordinates.class differ diff --git a/bin/ij/plugin/XY_Reader.class b/bin/ij/plugin/XY_Reader.class new file mode 100644 index 0000000..d7dd803 Binary files /dev/null and b/bin/ij/plugin/XY_Reader.class differ diff --git a/bin/ij/plugin/ZAxisProfiler.class b/bin/ij/plugin/ZAxisProfiler.class new file mode 100644 index 0000000..4d11927 Binary files /dev/null and b/bin/ij/plugin/ZAxisProfiler.class differ diff --git a/bin/ij/plugin/ZProjector$AverageIntensity.class b/bin/ij/plugin/ZProjector$AverageIntensity.class new file mode 100644 index 0000000..84b6a0e Binary files /dev/null and b/bin/ij/plugin/ZProjector$AverageIntensity.class differ diff --git a/bin/ij/plugin/ZProjector$MaxIntensity.class b/bin/ij/plugin/ZProjector$MaxIntensity.class new file mode 100644 index 0000000..4f20794 Binary files /dev/null and b/bin/ij/plugin/ZProjector$MaxIntensity.class differ diff --git a/bin/ij/plugin/ZProjector$MinIntensity.class b/bin/ij/plugin/ZProjector$MinIntensity.class new file mode 100644 index 0000000..d8ca9ef Binary files /dev/null and b/bin/ij/plugin/ZProjector$MinIntensity.class differ diff --git a/bin/ij/plugin/ZProjector$RayFunction.class b/bin/ij/plugin/ZProjector$RayFunction.class new file mode 100644 index 0000000..7ec894f Binary files /dev/null and b/bin/ij/plugin/ZProjector$RayFunction.class differ diff --git a/bin/ij/plugin/ZProjector$StandardDeviation.class b/bin/ij/plugin/ZProjector$StandardDeviation.class new file mode 100644 index 0000000..b5153de Binary files /dev/null and b/bin/ij/plugin/ZProjector$StandardDeviation.class differ diff --git a/bin/ij/plugin/ZProjector.class b/bin/ij/plugin/ZProjector.class new file mode 100644 index 0000000..cd10655 Binary files /dev/null and b/bin/ij/plugin/ZProjector.class differ diff --git a/bin/ij/plugin/Zoom.class b/bin/ij/plugin/Zoom.class new file mode 100644 index 0000000..bdae61a Binary files /dev/null and b/bin/ij/plugin/Zoom.class differ diff --git a/bin/ij/plugin/filter/AVI_Writer$RaOutputStream.class b/bin/ij/plugin/filter/AVI_Writer$RaOutputStream.class new file mode 100644 index 0000000..ed95f82 Binary files /dev/null and b/bin/ij/plugin/filter/AVI_Writer$RaOutputStream.class differ diff --git a/bin/ij/plugin/filter/AVI_Writer.class b/bin/ij/plugin/filter/AVI_Writer.class new file mode 100644 index 0000000..f7881e3 Binary files /dev/null and b/bin/ij/plugin/filter/AVI_Writer.class differ diff --git a/bin/ij/plugin/filter/Analyzer.class b/bin/ij/plugin/filter/Analyzer.class new file mode 100644 index 0000000..1a3df07 Binary files /dev/null and b/bin/ij/plugin/filter/Analyzer.class differ diff --git a/bin/ij/plugin/filter/BackgroundSubtracter.class b/bin/ij/plugin/filter/BackgroundSubtracter.class new file mode 100644 index 0000000..ef30254 Binary files /dev/null and b/bin/ij/plugin/filter/BackgroundSubtracter.class differ diff --git a/bin/ij/plugin/filter/Benchmark.class b/bin/ij/plugin/filter/Benchmark.class new file mode 100644 index 0000000..261ceff Binary files /dev/null and b/bin/ij/plugin/filter/Benchmark.class differ diff --git a/bin/ij/plugin/filter/Binary.class b/bin/ij/plugin/filter/Binary.class new file mode 100644 index 0000000..c36e4ae Binary files /dev/null and b/bin/ij/plugin/filter/Binary.class differ diff --git a/bin/ij/plugin/filter/Calibrator.class b/bin/ij/plugin/filter/Calibrator.class new file mode 100644 index 0000000..c7d8b09 Binary files /dev/null and b/bin/ij/plugin/filter/Calibrator.class differ diff --git a/bin/ij/plugin/filter/Convolver.class b/bin/ij/plugin/filter/Convolver.class new file mode 100644 index 0000000..1b959a5 Binary files /dev/null and b/bin/ij/plugin/filter/Convolver.class differ diff --git a/bin/ij/plugin/filter/Duplicater.class b/bin/ij/plugin/filter/Duplicater.class new file mode 100644 index 0000000..cf2f44b Binary files /dev/null and b/bin/ij/plugin/filter/Duplicater.class differ diff --git a/bin/ij/plugin/filter/EDM.class b/bin/ij/plugin/filter/EDM.class new file mode 100644 index 0000000..84a1223 Binary files /dev/null and b/bin/ij/plugin/filter/EDM.class differ diff --git a/bin/ij/plugin/filter/ExtendedPlugInFilter.class b/bin/ij/plugin/filter/ExtendedPlugInFilter.class new file mode 100644 index 0000000..0b23305 Binary files /dev/null and b/bin/ij/plugin/filter/ExtendedPlugInFilter.class differ diff --git a/bin/ij/plugin/filter/FFTCustomFilter.class b/bin/ij/plugin/filter/FFTCustomFilter.class new file mode 100644 index 0000000..07108cb Binary files /dev/null and b/bin/ij/plugin/filter/FFTCustomFilter.class differ diff --git a/bin/ij/plugin/filter/FFTFilter.class b/bin/ij/plugin/filter/FFTFilter.class new file mode 100644 index 0000000..86760a3 Binary files /dev/null and b/bin/ij/plugin/filter/FFTFilter.class differ diff --git a/bin/ij/plugin/filter/Filler.class b/bin/ij/plugin/filter/Filler.class new file mode 100644 index 0000000..8e6fd47 Binary files /dev/null and b/bin/ij/plugin/filter/Filler.class differ diff --git a/bin/ij/plugin/filter/Filters.class b/bin/ij/plugin/filter/Filters.class new file mode 100644 index 0000000..d8584f4 Binary files /dev/null and b/bin/ij/plugin/filter/Filters.class differ diff --git a/bin/ij/plugin/filter/FractalBoxCounter.class b/bin/ij/plugin/filter/FractalBoxCounter.class new file mode 100644 index 0000000..aba81ee Binary files /dev/null and b/bin/ij/plugin/filter/FractalBoxCounter.class differ diff --git a/bin/ij/plugin/filter/GaussianBlur$1.class b/bin/ij/plugin/filter/GaussianBlur$1.class new file mode 100644 index 0000000..fd02334 Binary files /dev/null and b/bin/ij/plugin/filter/GaussianBlur$1.class differ diff --git a/bin/ij/plugin/filter/GaussianBlur.class b/bin/ij/plugin/filter/GaussianBlur.class new file mode 100644 index 0000000..3337618 Binary files /dev/null and b/bin/ij/plugin/filter/GaussianBlur.class differ diff --git a/bin/ij/plugin/filter/ImageMath.class b/bin/ij/plugin/filter/ImageMath.class new file mode 100644 index 0000000..d7da5ff Binary files /dev/null and b/bin/ij/plugin/filter/ImageMath.class differ diff --git a/bin/ij/plugin/filter/ImageProperties.class b/bin/ij/plugin/filter/ImageProperties.class new file mode 100644 index 0000000..7d0feb6 Binary files /dev/null and b/bin/ij/plugin/filter/ImageProperties.class differ diff --git a/bin/ij/plugin/filter/Info.class b/bin/ij/plugin/filter/Info.class new file mode 100644 index 0000000..73d53ec Binary files /dev/null and b/bin/ij/plugin/filter/Info.class differ diff --git a/bin/ij/plugin/filter/LineGraphAnalyzer.class b/bin/ij/plugin/filter/LineGraphAnalyzer.class new file mode 100644 index 0000000..aea4849 Binary files /dev/null and b/bin/ij/plugin/filter/LineGraphAnalyzer.class differ diff --git a/bin/ij/plugin/filter/LutApplier.class b/bin/ij/plugin/filter/LutApplier.class new file mode 100644 index 0000000..c0f617d Binary files /dev/null and b/bin/ij/plugin/filter/LutApplier.class differ diff --git a/bin/ij/plugin/filter/LutViewer.class b/bin/ij/plugin/filter/LutViewer.class new file mode 100644 index 0000000..757f363 Binary files /dev/null and b/bin/ij/plugin/filter/LutViewer.class differ diff --git a/bin/ij/plugin/filter/LutWindow.class b/bin/ij/plugin/filter/LutWindow.class new file mode 100644 index 0000000..89124be Binary files /dev/null and b/bin/ij/plugin/filter/LutWindow.class differ diff --git a/bin/ij/plugin/filter/MaximumFinder.class b/bin/ij/plugin/filter/MaximumFinder.class new file mode 100644 index 0000000..9358543 Binary files /dev/null and b/bin/ij/plugin/filter/MaximumFinder.class differ diff --git a/bin/ij/plugin/filter/ParticleAnalyzer.class b/bin/ij/plugin/filter/ParticleAnalyzer.class new file mode 100644 index 0000000..eff6f00 Binary files /dev/null and b/bin/ij/plugin/filter/ParticleAnalyzer.class differ diff --git a/bin/ij/plugin/filter/PlugInFilter.class b/bin/ij/plugin/filter/PlugInFilter.class new file mode 100644 index 0000000..6eb96cc Binary files /dev/null and b/bin/ij/plugin/filter/PlugInFilter.class differ diff --git a/bin/ij/plugin/filter/PlugInFilterRunner.class b/bin/ij/plugin/filter/PlugInFilterRunner.class new file mode 100644 index 0000000..ce13e24 Binary files /dev/null and b/bin/ij/plugin/filter/PlugInFilterRunner.class differ diff --git a/bin/ij/plugin/filter/Printer.class b/bin/ij/plugin/filter/Printer.class new file mode 100644 index 0000000..5b48739 Binary files /dev/null and b/bin/ij/plugin/filter/Printer.class differ diff --git a/bin/ij/plugin/filter/RGBStackSplitter.class b/bin/ij/plugin/filter/RGBStackSplitter.class new file mode 100644 index 0000000..557147e Binary files /dev/null and b/bin/ij/plugin/filter/RGBStackSplitter.class differ diff --git a/bin/ij/plugin/filter/RankFilters$1.class b/bin/ij/plugin/filter/RankFilters$1.class new file mode 100644 index 0000000..b756d55 Binary files /dev/null and b/bin/ij/plugin/filter/RankFilters$1.class differ diff --git a/bin/ij/plugin/filter/RankFilters.class b/bin/ij/plugin/filter/RankFilters.class new file mode 100644 index 0000000..6fd8cd4 Binary files /dev/null and b/bin/ij/plugin/filter/RankFilters.class differ diff --git a/bin/ij/plugin/filter/RoiWriter.class b/bin/ij/plugin/filter/RoiWriter.class new file mode 100644 index 0000000..076ac9c Binary files /dev/null and b/bin/ij/plugin/filter/RoiWriter.class differ diff --git a/bin/ij/plugin/filter/RollingBall.class b/bin/ij/plugin/filter/RollingBall.class new file mode 100644 index 0000000..6900169 Binary files /dev/null and b/bin/ij/plugin/filter/RollingBall.class differ diff --git a/bin/ij/plugin/filter/Rotator.class b/bin/ij/plugin/filter/Rotator.class new file mode 100644 index 0000000..b27d20a Binary files /dev/null and b/bin/ij/plugin/filter/Rotator.class differ diff --git a/bin/ij/plugin/filter/SaltAndPepper.class b/bin/ij/plugin/filter/SaltAndPepper.class new file mode 100644 index 0000000..05abadb Binary files /dev/null and b/bin/ij/plugin/filter/SaltAndPepper.class differ diff --git a/bin/ij/plugin/filter/ScaleDialog.class b/bin/ij/plugin/filter/ScaleDialog.class new file mode 100644 index 0000000..2b1d071 Binary files /dev/null and b/bin/ij/plugin/filter/ScaleDialog.class differ diff --git a/bin/ij/plugin/filter/SetScaleDialog.class b/bin/ij/plugin/filter/SetScaleDialog.class new file mode 100644 index 0000000..2f866c1 Binary files /dev/null and b/bin/ij/plugin/filter/SetScaleDialog.class differ diff --git a/bin/ij/plugin/filter/Shadows.class b/bin/ij/plugin/filter/Shadows.class new file mode 100644 index 0000000..27303fa Binary files /dev/null and b/bin/ij/plugin/filter/Shadows.class differ diff --git a/bin/ij/plugin/filter/StackLabeler.class b/bin/ij/plugin/filter/StackLabeler.class new file mode 100644 index 0000000..74f8109 Binary files /dev/null and b/bin/ij/plugin/filter/StackLabeler.class differ diff --git a/bin/ij/plugin/filter/ThresholdToSelection$Outline.class b/bin/ij/plugin/filter/ThresholdToSelection$Outline.class new file mode 100644 index 0000000..0901ab1 Binary files /dev/null and b/bin/ij/plugin/filter/ThresholdToSelection$Outline.class differ diff --git a/bin/ij/plugin/filter/ThresholdToSelection.class b/bin/ij/plugin/filter/ThresholdToSelection.class new file mode 100644 index 0000000..276f8e4 Binary files /dev/null and b/bin/ij/plugin/filter/ThresholdToSelection.class differ diff --git a/bin/ij/plugin/filter/Transformer.class b/bin/ij/plugin/filter/Transformer.class new file mode 100644 index 0000000..3b19d9e Binary files /dev/null and b/bin/ij/plugin/filter/Transformer.class differ diff --git a/bin/ij/plugin/filter/Translator.class b/bin/ij/plugin/filter/Translator.class new file mode 100644 index 0000000..778c4cc Binary files /dev/null and b/bin/ij/plugin/filter/Translator.class differ diff --git a/bin/ij/plugin/filter/UnsharpMask.class b/bin/ij/plugin/filter/UnsharpMask.class new file mode 100644 index 0000000..5bc74ef Binary files /dev/null and b/bin/ij/plugin/filter/UnsharpMask.class differ diff --git a/bin/ij/plugin/filter/Writer.class b/bin/ij/plugin/filter/Writer.class new file mode 100644 index 0000000..cae3fef Binary files /dev/null and b/bin/ij/plugin/filter/Writer.class differ diff --git a/bin/ij/plugin/filter/XYWriter.class b/bin/ij/plugin/filter/XYWriter.class new file mode 100644 index 0000000..c02f58e Binary files /dev/null and b/bin/ij/plugin/filter/XYWriter.class differ diff --git a/bin/ij/plugin/frame/Channels.class b/bin/ij/plugin/frame/Channels.class new file mode 100644 index 0000000..e2edc93 Binary files /dev/null and b/bin/ij/plugin/frame/Channels.class differ diff --git a/bin/ij/plugin/frame/ColorCanvas.class b/bin/ij/plugin/frame/ColorCanvas.class new file mode 100644 index 0000000..7c7c127 Binary files /dev/null and b/bin/ij/plugin/frame/ColorCanvas.class differ diff --git a/bin/ij/plugin/frame/ColorGenerator.class b/bin/ij/plugin/frame/ColorGenerator.class new file mode 100644 index 0000000..d4dcfc0 Binary files /dev/null and b/bin/ij/plugin/frame/ColorGenerator.class differ diff --git a/bin/ij/plugin/frame/ColorPicker.class b/bin/ij/plugin/frame/ColorPicker.class new file mode 100644 index 0000000..a9ed0e0 Binary files /dev/null and b/bin/ij/plugin/frame/ColorPicker.class differ diff --git a/bin/ij/plugin/frame/ColorThresholder$BandPlot.class b/bin/ij/plugin/frame/ColorThresholder$BandPlot.class new file mode 100644 index 0000000..811fc95 Binary files /dev/null and b/bin/ij/plugin/frame/ColorThresholder$BandPlot.class differ diff --git a/bin/ij/plugin/frame/ColorThresholder.class b/bin/ij/plugin/frame/ColorThresholder.class new file mode 100644 index 0000000..a9c9e3e Binary files /dev/null and b/bin/ij/plugin/frame/ColorThresholder.class differ diff --git a/bin/ij/plugin/frame/Commands.class b/bin/ij/plugin/frame/Commands.class new file mode 100644 index 0000000..310f785 Binary files /dev/null and b/bin/ij/plugin/frame/Commands.class differ diff --git a/bin/ij/plugin/frame/ContrastAdjuster.class b/bin/ij/plugin/frame/ContrastAdjuster.class new file mode 100644 index 0000000..68bfba0 Binary files /dev/null and b/bin/ij/plugin/frame/ContrastAdjuster.class differ diff --git a/bin/ij/plugin/frame/ContrastPlot.class b/bin/ij/plugin/frame/ContrastPlot.class new file mode 100644 index 0000000..aaccc3f Binary files /dev/null and b/bin/ij/plugin/frame/ContrastPlot.class differ diff --git a/bin/ij/plugin/frame/DisplayChangeEvent.class b/bin/ij/plugin/frame/DisplayChangeEvent.class new file mode 100644 index 0000000..76362dd Binary files /dev/null and b/bin/ij/plugin/frame/DisplayChangeEvent.class differ diff --git a/bin/ij/plugin/frame/DisplayChangeListener.class b/bin/ij/plugin/frame/DisplayChangeListener.class new file mode 100644 index 0000000..6f4ca1f Binary files /dev/null and b/bin/ij/plugin/frame/DisplayChangeListener.class differ diff --git a/bin/ij/plugin/frame/Editor.class b/bin/ij/plugin/frame/Editor.class new file mode 100644 index 0000000..94989ce Binary files /dev/null and b/bin/ij/plugin/frame/Editor.class differ diff --git a/bin/ij/plugin/frame/Fitter$1.class b/bin/ij/plugin/frame/Fitter$1.class new file mode 100644 index 0000000..c2cf51b Binary files /dev/null and b/bin/ij/plugin/frame/Fitter$1.class differ diff --git a/bin/ij/plugin/frame/Fitter.class b/bin/ij/plugin/frame/Fitter.class new file mode 100644 index 0000000..b45ac04 Binary files /dev/null and b/bin/ij/plugin/frame/Fitter.class differ diff --git a/bin/ij/plugin/frame/IJEventMulticaster.class b/bin/ij/plugin/frame/IJEventMulticaster.class new file mode 100644 index 0000000..ac153ba Binary files /dev/null and b/bin/ij/plugin/frame/IJEventMulticaster.class differ diff --git a/bin/ij/plugin/frame/LineWidthAdjuster.class b/bin/ij/plugin/frame/LineWidthAdjuster.class new file mode 100644 index 0000000..c377da2 Binary files /dev/null and b/bin/ij/plugin/frame/LineWidthAdjuster.class differ diff --git a/bin/ij/plugin/frame/MemoryMonitor$PlotCanvas.class b/bin/ij/plugin/frame/MemoryMonitor$PlotCanvas.class new file mode 100644 index 0000000..0b840e7 Binary files /dev/null and b/bin/ij/plugin/frame/MemoryMonitor$PlotCanvas.class differ diff --git a/bin/ij/plugin/frame/MemoryMonitor.class b/bin/ij/plugin/frame/MemoryMonitor.class new file mode 100644 index 0000000..fa3edb0 Binary files /dev/null and b/bin/ij/plugin/frame/MemoryMonitor.class differ diff --git a/bin/ij/plugin/frame/PasteController.class b/bin/ij/plugin/frame/PasteController.class new file mode 100644 index 0000000..889ae4d Binary files /dev/null and b/bin/ij/plugin/frame/PasteController.class differ diff --git a/bin/ij/plugin/frame/PlugInDialog.class b/bin/ij/plugin/frame/PlugInDialog.class new file mode 100644 index 0000000..b0bf6b9 Binary files /dev/null and b/bin/ij/plugin/frame/PlugInDialog.class differ diff --git a/bin/ij/plugin/frame/PlugInFrame.class b/bin/ij/plugin/frame/PlugInFrame.class new file mode 100644 index 0000000..e0510e2 Binary files /dev/null and b/bin/ij/plugin/frame/PlugInFrame.class differ diff --git a/bin/ij/plugin/frame/Recorder.class b/bin/ij/plugin/frame/Recorder.class new file mode 100644 index 0000000..0496c29 Binary files /dev/null and b/bin/ij/plugin/frame/Recorder.class differ diff --git a/bin/ij/plugin/frame/RoiManager$1.class b/bin/ij/plugin/frame/RoiManager$1.class new file mode 100644 index 0000000..1b0df43 Binary files /dev/null and b/bin/ij/plugin/frame/RoiManager$1.class differ diff --git a/bin/ij/plugin/frame/RoiManager$2.class b/bin/ij/plugin/frame/RoiManager$2.class new file mode 100644 index 0000000..21e1c82 Binary files /dev/null and b/bin/ij/plugin/frame/RoiManager$2.class differ diff --git a/bin/ij/plugin/frame/RoiManager$3.class b/bin/ij/plugin/frame/RoiManager$3.class new file mode 100644 index 0000000..4367a72 Binary files /dev/null and b/bin/ij/plugin/frame/RoiManager$3.class differ diff --git a/bin/ij/plugin/frame/RoiManager$4.class b/bin/ij/plugin/frame/RoiManager$4.class new file mode 100644 index 0000000..0cd66c9 Binary files /dev/null and b/bin/ij/plugin/frame/RoiManager$4.class differ diff --git a/bin/ij/plugin/frame/RoiManager$MultiMeasureRunner.class b/bin/ij/plugin/frame/RoiManager$MultiMeasureRunner.class new file mode 100644 index 0000000..6200d61 Binary files /dev/null and b/bin/ij/plugin/frame/RoiManager$MultiMeasureRunner.class differ diff --git a/bin/ij/plugin/frame/RoiManager.class b/bin/ij/plugin/frame/RoiManager.class new file mode 100644 index 0000000..6078557 Binary files /dev/null and b/bin/ij/plugin/frame/RoiManager.class differ diff --git a/bin/ij/plugin/frame/SyncWindows.class b/bin/ij/plugin/frame/SyncWindows.class new file mode 100644 index 0000000..fbc999f Binary files /dev/null and b/bin/ij/plugin/frame/SyncWindows.class differ diff --git a/bin/ij/plugin/frame/ThresholdAdjuster.class b/bin/ij/plugin/frame/ThresholdAdjuster.class new file mode 100644 index 0000000..94af7ab Binary files /dev/null and b/bin/ij/plugin/frame/ThresholdAdjuster.class differ diff --git a/bin/ij/plugin/frame/ThresholdPlot.class b/bin/ij/plugin/frame/ThresholdPlot.class new file mode 100644 index 0000000..67af72f Binary files /dev/null and b/bin/ij/plugin/frame/ThresholdPlot.class differ diff --git a/bin/ij/plugin/frame/TrimmedLabel.class b/bin/ij/plugin/frame/TrimmedLabel.class new file mode 100644 index 0000000..78b96b6 Binary files /dev/null and b/bin/ij/plugin/frame/TrimmedLabel.class differ diff --git a/bin/ij/plugin/tool/ArrowTool.class b/bin/ij/plugin/tool/ArrowTool.class new file mode 100644 index 0000000..845dfc1 Binary files /dev/null and b/bin/ij/plugin/tool/ArrowTool.class differ diff --git a/bin/ij/plugin/tool/BrushTool$Options.class b/bin/ij/plugin/tool/BrushTool$Options.class new file mode 100644 index 0000000..19ec1be Binary files /dev/null and b/bin/ij/plugin/tool/BrushTool$Options.class differ diff --git a/bin/ij/plugin/tool/BrushTool.class b/bin/ij/plugin/tool/BrushTool.class new file mode 100644 index 0000000..15d842c Binary files /dev/null and b/bin/ij/plugin/tool/BrushTool.class differ diff --git a/bin/ij/plugin/tool/MacroToolRunner.class b/bin/ij/plugin/tool/MacroToolRunner.class new file mode 100644 index 0000000..9d5f7c1 Binary files /dev/null and b/bin/ij/plugin/tool/MacroToolRunner.class differ diff --git a/bin/ij/plugin/tool/OverlayBrushTool$Options.class b/bin/ij/plugin/tool/OverlayBrushTool$Options.class new file mode 100644 index 0000000..6ee184c Binary files /dev/null and b/bin/ij/plugin/tool/OverlayBrushTool$Options.class differ diff --git a/bin/ij/plugin/tool/OverlayBrushTool.class b/bin/ij/plugin/tool/OverlayBrushTool.class new file mode 100644 index 0000000..58fb0aa Binary files /dev/null and b/bin/ij/plugin/tool/OverlayBrushTool.class differ diff --git a/bin/ij/plugin/tool/PixelInspectionTool.class b/bin/ij/plugin/tool/PixelInspectionTool.class new file mode 100644 index 0000000..6fae179 Binary files /dev/null and b/bin/ij/plugin/tool/PixelInspectionTool.class differ diff --git a/bin/ij/plugin/tool/PixelInspector.class b/bin/ij/plugin/tool/PixelInspector.class new file mode 100644 index 0000000..f64498c Binary files /dev/null and b/bin/ij/plugin/tool/PixelInspector.class differ diff --git a/bin/ij/plugin/tool/PlugInTool.class b/bin/ij/plugin/tool/PlugInTool.class new file mode 100644 index 0000000..310a914 Binary files /dev/null and b/bin/ij/plugin/tool/PlugInTool.class differ diff --git a/bin/ij/plugin/tool/RoiRotationTool.class b/bin/ij/plugin/tool/RoiRotationTool.class new file mode 100644 index 0000000..dbb5ce8 Binary files /dev/null and b/bin/ij/plugin/tool/RoiRotationTool.class differ diff --git a/bin/ij/process/AutoThresholder$Method.class b/bin/ij/process/AutoThresholder$Method.class new file mode 100644 index 0000000..1a15826 Binary files /dev/null and b/bin/ij/process/AutoThresholder$Method.class differ diff --git a/bin/ij/process/AutoThresholder.class b/bin/ij/process/AutoThresholder.class new file mode 100644 index 0000000..734bf86 Binary files /dev/null and b/bin/ij/process/AutoThresholder.class differ diff --git a/bin/ij/process/BinaryInterpolator$IDT.class b/bin/ij/process/BinaryInterpolator$IDT.class new file mode 100644 index 0000000..e43355b Binary files /dev/null and b/bin/ij/process/BinaryInterpolator$IDT.class differ diff --git a/bin/ij/process/BinaryInterpolator.class b/bin/ij/process/BinaryInterpolator.class new file mode 100644 index 0000000..b30cc8f Binary files /dev/null and b/bin/ij/process/BinaryInterpolator.class differ diff --git a/bin/ij/process/BinaryProcessor.class b/bin/ij/process/BinaryProcessor.class new file mode 100644 index 0000000..fbdbcfd Binary files /dev/null and b/bin/ij/process/BinaryProcessor.class differ diff --git a/bin/ij/process/Blitter.class b/bin/ij/process/Blitter.class new file mode 100644 index 0000000..cc3f0d1 Binary files /dev/null and b/bin/ij/process/Blitter.class differ diff --git a/bin/ij/process/ByteBlitter.class b/bin/ij/process/ByteBlitter.class new file mode 100644 index 0000000..c4c6c62 Binary files /dev/null and b/bin/ij/process/ByteBlitter.class differ diff --git a/bin/ij/process/ByteProcessor.class b/bin/ij/process/ByteProcessor.class new file mode 100644 index 0000000..deb4286 Binary files /dev/null and b/bin/ij/process/ByteProcessor.class differ diff --git a/bin/ij/process/ByteStatistics.class b/bin/ij/process/ByteStatistics.class new file mode 100644 index 0000000..7054a31 Binary files /dev/null and b/bin/ij/process/ByteStatistics.class differ diff --git a/bin/ij/process/ColorBlitter.class b/bin/ij/process/ColorBlitter.class new file mode 100644 index 0000000..340a38a Binary files /dev/null and b/bin/ij/process/ColorBlitter.class differ diff --git a/bin/ij/process/ColorProcessor.class b/bin/ij/process/ColorProcessor.class new file mode 100644 index 0000000..2c2920b Binary files /dev/null and b/bin/ij/process/ColorProcessor.class differ diff --git a/bin/ij/process/ColorSpaceConverter.class b/bin/ij/process/ColorSpaceConverter.class new file mode 100644 index 0000000..eeba99f Binary files /dev/null and b/bin/ij/process/ColorSpaceConverter.class differ diff --git a/bin/ij/process/ColorStatistics.class b/bin/ij/process/ColorStatistics.class new file mode 100644 index 0000000..2c3ae69 Binary files /dev/null and b/bin/ij/process/ColorStatistics.class differ diff --git a/bin/ij/process/Cube.class b/bin/ij/process/Cube.class new file mode 100644 index 0000000..87b103b Binary files /dev/null and b/bin/ij/process/Cube.class differ diff --git a/bin/ij/process/DownsizeTable.class b/bin/ij/process/DownsizeTable.class new file mode 100644 index 0000000..76883ea Binary files /dev/null and b/bin/ij/process/DownsizeTable.class differ diff --git a/bin/ij/process/EllipseFitter.class b/bin/ij/process/EllipseFitter.class new file mode 100644 index 0000000..b57c7e9 Binary files /dev/null and b/bin/ij/process/EllipseFitter.class differ diff --git a/bin/ij/process/FHT.class b/bin/ij/process/FHT.class new file mode 100644 index 0000000..8bffdf4 Binary files /dev/null and b/bin/ij/process/FHT.class differ diff --git a/bin/ij/process/FloatBlitter.class b/bin/ij/process/FloatBlitter.class new file mode 100644 index 0000000..879ed87 Binary files /dev/null and b/bin/ij/process/FloatBlitter.class differ diff --git a/bin/ij/process/FloatPolygon.class b/bin/ij/process/FloatPolygon.class new file mode 100644 index 0000000..a39d3f1 Binary files /dev/null and b/bin/ij/process/FloatPolygon.class differ diff --git a/bin/ij/process/FloatProcessor.class b/bin/ij/process/FloatProcessor.class new file mode 100644 index 0000000..ee25fab Binary files /dev/null and b/bin/ij/process/FloatProcessor.class differ diff --git a/bin/ij/process/FloatStatistics.class b/bin/ij/process/FloatStatistics.class new file mode 100644 index 0000000..2d77347 Binary files /dev/null and b/bin/ij/process/FloatStatistics.class differ diff --git a/bin/ij/process/FloodFiller.class b/bin/ij/process/FloodFiller.class new file mode 100644 index 0000000..5ad7c69 Binary files /dev/null and b/bin/ij/process/FloodFiller.class differ diff --git a/bin/ij/process/ImageConverter.class b/bin/ij/process/ImageConverter.class new file mode 100644 index 0000000..e0e25bf Binary files /dev/null and b/bin/ij/process/ImageConverter.class differ diff --git a/bin/ij/process/ImageProcessor.class b/bin/ij/process/ImageProcessor.class new file mode 100644 index 0000000..5e613ce Binary files /dev/null and b/bin/ij/process/ImageProcessor.class differ diff --git a/bin/ij/process/ImageStatistics.class b/bin/ij/process/ImageStatistics.class new file mode 100644 index 0000000..eb9b972 Binary files /dev/null and b/bin/ij/process/ImageStatistics.class differ diff --git a/bin/ij/process/IntProcessor.class b/bin/ij/process/IntProcessor.class new file mode 100644 index 0000000..e9bfb5e Binary files /dev/null and b/bin/ij/process/IntProcessor.class differ diff --git a/bin/ij/process/LUT.class b/bin/ij/process/LUT.class new file mode 100644 index 0000000..47723ab Binary files /dev/null and b/bin/ij/process/LUT.class differ diff --git a/bin/ij/process/MedianCut.class b/bin/ij/process/MedianCut.class new file mode 100644 index 0000000..fe9dcd7 Binary files /dev/null and b/bin/ij/process/MedianCut.class differ diff --git a/bin/ij/process/PolygonFiller.class b/bin/ij/process/PolygonFiller.class new file mode 100644 index 0000000..7934ad2 Binary files /dev/null and b/bin/ij/process/PolygonFiller.class differ diff --git a/bin/ij/process/ShortBlitter.class b/bin/ij/process/ShortBlitter.class new file mode 100644 index 0000000..41af3d3 Binary files /dev/null and b/bin/ij/process/ShortBlitter.class differ diff --git a/bin/ij/process/ShortProcessor.class b/bin/ij/process/ShortProcessor.class new file mode 100644 index 0000000..6bc882d Binary files /dev/null and b/bin/ij/process/ShortProcessor.class differ diff --git a/bin/ij/process/ShortStatistics.class b/bin/ij/process/ShortStatistics.class new file mode 100644 index 0000000..43eb1ee Binary files /dev/null and b/bin/ij/process/ShortStatistics.class differ diff --git a/bin/ij/process/StackConverter.class b/bin/ij/process/StackConverter.class new file mode 100644 index 0000000..534f330 Binary files /dev/null and b/bin/ij/process/StackConverter.class differ diff --git a/bin/ij/process/StackProcessor.class b/bin/ij/process/StackProcessor.class new file mode 100644 index 0000000..aa0803b Binary files /dev/null and b/bin/ij/process/StackProcessor.class differ diff --git a/bin/ij/process/StackStatistics.class b/bin/ij/process/StackStatistics.class new file mode 100644 index 0000000..1b45fda Binary files /dev/null and b/bin/ij/process/StackStatistics.class differ diff --git a/bin/ij/process/TypeConverter.class b/bin/ij/process/TypeConverter.class new file mode 100644 index 0000000..e27534f Binary files /dev/null and b/bin/ij/process/TypeConverter.class differ diff --git a/bin/ij/text/TableListener.class b/bin/ij/text/TableListener.class new file mode 100644 index 0000000..3f0f27a Binary files /dev/null and b/bin/ij/text/TableListener.class differ diff --git a/bin/ij/text/TextCanvas.class b/bin/ij/text/TextCanvas.class new file mode 100644 index 0000000..16c8955 Binary files /dev/null and b/bin/ij/text/TextCanvas.class differ diff --git a/bin/ij/text/TextPanel.class b/bin/ij/text/TextPanel.class new file mode 100644 index 0000000..77ea6ba Binary files /dev/null and b/bin/ij/text/TextPanel.class differ diff --git a/bin/ij/text/TextWindow.class b/bin/ij/text/TextWindow.class new file mode 100644 index 0000000..02b4fba Binary files /dev/null and b/bin/ij/text/TextWindow.class differ diff --git a/bin/ij/util/ArrayUtil.class b/bin/ij/util/ArrayUtil.class new file mode 100644 index 0000000..3f16c36 Binary files /dev/null and b/bin/ij/util/ArrayUtil.class differ diff --git a/bin/ij/util/DicomTools.class b/bin/ij/util/DicomTools.class new file mode 100644 index 0000000..5c51495 Binary files /dev/null and b/bin/ij/util/DicomTools.class differ diff --git a/bin/ij/util/FloatArray.class b/bin/ij/util/FloatArray.class new file mode 100644 index 0000000..cd3de35 Binary files /dev/null and b/bin/ij/util/FloatArray.class differ diff --git a/bin/ij/util/FontUtil.class b/bin/ij/util/FontUtil.class new file mode 100644 index 0000000..61c9f6c Binary files /dev/null and b/bin/ij/util/FontUtil.class differ diff --git a/bin/ij/util/IJMath.class b/bin/ij/util/IJMath.class new file mode 100644 index 0000000..8a5388a Binary files /dev/null and b/bin/ij/util/IJMath.class differ diff --git a/bin/ij/util/Java2.class b/bin/ij/util/Java2.class new file mode 100644 index 0000000..311830e Binary files /dev/null and b/bin/ij/util/Java2.class differ diff --git a/bin/ij/util/StringSorter.class b/bin/ij/util/StringSorter.class new file mode 100644 index 0000000..91391c7 Binary files /dev/null and b/bin/ij/util/StringSorter.class differ diff --git a/bin/ij/util/ThreadUtil$1.class b/bin/ij/util/ThreadUtil$1.class new file mode 100644 index 0000000..a6698f6 Binary files /dev/null and b/bin/ij/util/ThreadUtil$1.class differ diff --git a/bin/ij/util/ThreadUtil.class b/bin/ij/util/ThreadUtil.class new file mode 100644 index 0000000..37924d0 Binary files /dev/null and b/bin/ij/util/ThreadUtil.class differ diff --git a/bin/ij/util/Tools$1.class b/bin/ij/util/Tools$1.class new file mode 100644 index 0000000..469f9f7 Binary files /dev/null and b/bin/ij/util/Tools$1.class differ diff --git a/bin/ij/util/Tools$2.class b/bin/ij/util/Tools$2.class new file mode 100644 index 0000000..00de779 Binary files /dev/null and b/bin/ij/util/Tools$2.class differ diff --git a/bin/ij/util/Tools.class b/bin/ij/util/Tools.class new file mode 100644 index 0000000..4ba0ec4 Binary files /dev/null and b/bin/ij/util/Tools.class differ diff --git a/bin/ij/util/WildcardMatch.class b/bin/ij/util/WildcardMatch.class new file mode 100644 index 0000000..e120764 Binary files /dev/null and b/bin/ij/util/WildcardMatch.class differ diff --git a/images/about.jpg b/images/about.jpg new file mode 100644 index 0000000..3ae6414 Binary files /dev/null and b/images/about.jpg differ diff --git a/images/microscope.gif b/images/microscope.gif new file mode 100644 index 0000000..bb2c9f5 Binary files /dev/null and b/images/microscope.gif differ diff --git a/macros/AddParticles.txt b/macros/AddParticles.txt new file mode 100644 index 0000000..edd71cf --- /dev/null +++ b/macros/AddParticles.txt @@ -0,0 +1,12 @@ +// Adds particle analyzer ROIs to the ROI Manager + if (nResults==0) + return "Results table is empty"; + if (isNaN(getResult('XStart', 0))) + return "Run the particle analyzer with \"Clear Results\" \nand \"Record Starts\" checked, then try again."; + for (i=0; iInstall Macros) to reinstall after +// making changes. Double click on the tool icon (a circle) +// to set the radius of the circle. +// There is more information about macro tools at +// http://imagej.nih.gov/ij/developer/macro/macros.html#tools +// and many more examples at +// http://imagej.nih.gov/ij/macros/tools/ + +var radius = 20; + +macro "Circle Tool - C00cO11cc" { + getCursorLoc(x, y, z, flags); + makeOval(x-radius, y-radius, radius*2, radius*2); +} + +macro "Circle Tool Options" { + radius = getNumber("Radius: ", radius); +} diff --git a/macros/CommandFinderTool.txt b/macros/CommandFinderTool.txt new file mode 100644 index 0000000..4d673d4 --- /dev/null +++ b/macros/CommandFinderTool.txt @@ -0,0 +1,4 @@ + macro "Command Finder Action Tool - C037T0e17CTce17F" { + run("Find Commands..."); + } + diff --git a/macros/ConvertStackToBinary.txt b/macros/ConvertStackToBinary.txt new file mode 100644 index 0000000..faf0842 --- /dev/null +++ b/macros/ConvertStackToBinary.txt @@ -0,0 +1,19 @@ +// Converts a stack to binary using locally calculated thresholds + + setBatchMode(true); + run("Select None"); + run("8-bit"); + id = getImageID; + for (i=1; i<=nSlices; i++) { + setSlice(i); + run("Duplicate...", "title=temp"); + run("Convert to Mask"); + invertingLUT = is("Inverting LUT"); + run("Copy"); + close; + selectImage(id); + run("Paste"); + if (i==1 && invertingLUT != is("Inverting LUT")) + run("Invert LUT"); + } + run("Select None"); diff --git a/macros/DeveloperMenuTool.txt b/macros/DeveloperMenuTool.txt new file mode 100644 index 0000000..491546f --- /dev/null +++ b/macros/DeveloperMenuTool.txt @@ -0,0 +1,14 @@ + var dCmds = newMenu("Developer Menu Tool", + newArray("Record...", "Capture Screen ", "Monitor Memory...", "Find Commands...", + "Search...", "ImageJ Properties", "List Elements", "Debug Mode", "ImageJ Website...", + "-", "Image...", "Hyperstack...", "Macro", "Open...", + "-", "Blobs", "Fly Brain", "HeLa Cells (48-bit RGB)", "Image with Overlay", + "Mitosis (5D stack)", "T1 Head (16-bits)")); + + macro "Developer Menu Tool - C037T0b14DT9b12eTfb12v" { + cmd = getArgument(); + if (cmd=="Debug Mode") + setOption("DebugMode", true); + else if (cmd!="-") + run(cmd); + } diff --git a/macros/Filter_Plugin.src b/macros/Filter_Plugin.src new file mode 100644 index 0000000..d2389b4 --- /dev/null +++ b/macros/Filter_Plugin.src @@ -0,0 +1,19 @@ +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.plugin.filter.*; + +public class Filter_Plugin implements PlugInFilter { + ImagePlus imp; + + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + return DOES_ALL; + } + + public void run(ImageProcessor ip) { + ip.invert(); + } + +} diff --git a/macros/FloodFillTool.txt b/macros/FloodFillTool.txt new file mode 100644 index 0000000..6d97598 --- /dev/null +++ b/macros/FloodFillTool.txt @@ -0,0 +1,18 @@ + var floodType = call("ij.Prefs.get", "tool.flood", "8-connected"); + var alt = 8; + + macro "Flood Fill Tool -C037B21P085373b75d0L4d1aL3135L4050L6166D57D77D68D09D94Da7C123Da5La9abLb6bc" { + setupUndo(); + getCursorLoc(x, y, z, flags); + if (flags&alt!=0) + setColor(getValue("color.background")); + floodFill(x, y, floodType); + } + + macro 'Flood Fill Tool Options...' { + Dialog.create("Flood Fill Tool"); + Dialog.addChoice("Flood Type:", newArray("4-connected", "8-connected"), floodType); + Dialog.show(); + floodType = Dialog.getChoice(); + call("ij.Prefs.set", "tool.flood", floodType); + } diff --git a/macros/LUTMenuTool.txt b/macros/LUTMenuTool.txt new file mode 100644 index 0000000..2ce2add --- /dev/null +++ b/macros/LUTMenuTool.txt @@ -0,0 +1,19 @@ + var lutdir = getDirectory("startup")+"luts"+File.separator; + var luts = getLutMenu(); + var lCmds = newMenu("LUT Menu Tool", luts); + + macro "LUT Menu Tool - C037T0b10LT6b10UTeb10T" { + cmd = getArgument(); + if (cmd!="-") run(cmd); + } + + function getLutMenu() { + list = getList("LUTs"); + menu = newArray(3+list.length); + menu[0] = "Invert LUT"; + menu[1] = "Apply LUT"; + menu[2] = "-"; + for (i=0; i1 || width!=globalLineWidth) { + lineWidth=width; + globalLineWidth = width; + } + radius = maxOf(4,lineWidth*2); + getCursorLoc(x, y, z, flags); + if (flags&9>0) { // shift or alt + removeLabel(x,y); + exit(); + } + getDateAndTime(yr, mo, dw, d, h, m, s, ms); + uid = "labeltool_"+yr+""+mo+""+d+""+h+""+m+""+s+""+ms; + nbefore = Overlay.size; + getCursorLoc(x1, y1, z, flags); + setLineWidth(lineWidth); + while (flags&16>0) { + getCursorLoc(x1, y1, z, flags); + drawItem(); + wait(30); + while (Overlay.size>nbefore) + Overlay.removeSelection(Overlay.size-1); + } + drawItem(); + label =getString("Enter label", label); + while (Overlay.size>nbefore) + Overlay.removeSelection(Overlay.size-1); + drawItem(); + } + + function drawItem() { + makeOval(x-radius, y-radius, radius*2, radius*2); + Roi.setName(uid); + Overlay.addSelection("",0,""+hexCol()); + makeLine(x, y,x1,y1,x1+(((x1=x)*1))*getStringWidth(label),y1); + Roi.setName(uid); + Overlay.addSelection(""+hexCol(), lineWidth); + setFont("user"); + makeText(label, x1 - (x1=0) { + Overlay.activateSelection(index); + name = Roi.getName(); + Overlay.removeRois(name); + Roi.remove; + } + } + + macro "Label Tool (double click for options) Options" { + Dialog.create("Label Maker Tool Options"); + Dialog.addNumber("Line width:", lineWidth, 0, 3, "pixels"); + m1 = "Shift or alt click to remove a label.\n"; + m2 = "Double click on text tool to change\n"; + m3 = "font size and color."; + Dialog.setInsets(0, 0, 0); + Dialog.addMessage(m1+m2+m3); + Dialog.show(); + lineWidth = Dialog.getNumber(); + } + diff --git a/macros/MagicMontageTools.txt b/macros/MagicMontageTools.txt new file mode 100644 index 0000000..9b28037 --- /dev/null +++ b/macros/MagicMontageTools.txt @@ -0,0 +1,448 @@ +//--version--1.6 +// 1.6 adds the '?' button that points to the wiki page +// panel labels are now drawn on an overlay +// added overlay commands and copy to system clipboard to the rightclick menu +// Montage tools for easy montage manipulation +// jerome.mutterer at ibmp.fr + +var str="ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +var lcas=false; +var antialiasedLabels = true; +var n=0; +var xoffset=0.05; +var yoffset=0.05; +var pos="Clicked quadrant"; + +var commands = newArray("Copy", "Paste","-", "Scale Bar...", + "Brightness/Contrast...", "-", "Extract Selected Panels","Crop Montage [F3]","-", + "Add Panel to Manager [F1]","Selected panels to stack [F2]", "Montage to Stack", "-", + "Fit Clipboard content into panel [F4]","Fill Panel with Clipboard content [F5]","-", + "Set Montage Layout","Change Montage Layout","-", + "Hide Overlay","Show Overlay", "Remove Overlay", "Flatten", "-", + "Copy to System")); + +var toolCmds = newMenu("Magic Montage Menu Tool",commands); +var pmCmds = newMenu("Popup Menu",commands); + +macro "Popup Menu" { + runCommand(); +} + +macro "Auto Montage Action Tool - C66fF0077C6f6F9977Cf66F9077C888F0977" { + setBatchMode(true); + b=bitDepth; + if ((b!=24)&&(nSlices==1)) { exit("Stack, Composite, or RGB image required.");} + if ((b==24)&&(nSlices==1)) { run("Make Composite"); b=8;} + Stack.getDimensions(width, height, channels, slices, frames); + getVoxelSize(xp,yp,zp,unit); + if (channels==1) { channels = channels* frames*slices; Stack.setDimensions(channels,1,1); } + id=getImageID; + t=getTitle; + if (b!=24) { + newImage("tempmont", "RGB", width, height,channels); + id2=getImageID; + for (i=1;i<=channels;i++) { + setPasteMode("copy"); + selectImage(id); + Stack.setChannel(i); + getLut(r,g,b); + run("Duplicate...", "title=temp"+i); + setLut(r,g,b); + run("RGB Color"); + run("Copy"); + selectImage(id2); + setSlice(i); + run("Paste"); + } + } + run("Make Montage...", "scale=1 border=0"); + rename(getTitle+" of "+t); + setVoxelSize(xp,yp,zp,unit); + setBatchMode(false); +} + +macro "Select Panels Tool - Cf44R0077R9077C888R9977R0977"{ + run("Select None"); + setPasteMode("copy"); + w = getWidth; + h = getHeight; + getCursorLoc(x, y, z, flags); + id=getImageID; + t=getTitle; + selectImage(id); + xn = info("xMontage"); + yn = info("yMontage"); + if ((xn==0)||(yn==0)) + exit; + xc = floor(x/(w/xn)); + yc = floor(y/(h/yn)); + makeRectangle(xc*(w/xn),yc*(h/yn),(w/xn),(h/yn)); + xstart = x; ystart = y; + x2=x; y2=y; + x2c=xc;y2c=yc; + while (flags&16 !=0) { + getCursorLoc(x, y, z, flags); + if (x!=x2 || y!=y2) { + x2c = floor(x/(w/xn)); + y2c = floor(y/(h/yn)); + makeRectangle(xc*(w/xn),yc*(h/yn),(w/xn)*(x2c-xc+1),(h/yn)*(y2c-yc+1)); + x2=x; y2=y; + wait(10); + } + } + setPasteMode("add"); +} + +macro "Extract Selected Panels"{ + t=getTitle; + xn = info("xMontage"); + yn = info("yMontage"); + pw = getWidth/xn; + ph = getHeight/yn; + run("Duplicate...", "title=[Extract of "+t+"]"); + setMetadata("Info","xMontage="+getWidth/pw+"\nyMontage="+getHeight/ph+"\n"); +} + +macro "Montage Shuffler Tool - C888R0077R9977C44fR0977R9077"{ + id=getImageID; + run("Select None"); + setPasteMode("copy"); + w = getWidth; + h = getHeight; + getCursorLoc(x, y, z, flags); + xn = info("xMontage"); + yn = info("yMontage"); + if ((xn==0)||(yn==0)) + exit; + xstart = x; ystart = y; + x2=x; y2=y; + while (flags&16 !=0) { + getCursorLoc(x, y, z, flags); + if (x!=x2 || y!=y2) spring(xstart, ystart, x, y); + x2=x; y2=y; + wait(10); + } + if (x!=xstart || y!=ystart) { + xext=0; + yext=0; + if (x>w) xext=1; + if (y>h) yext=1; + if ((xext>0)||(yext>0)) { + run("Canvas Size...", "width="+w+xext*(w/xn)+" height="+h+yext*(h/yn)+" position=Top-Left zero"); + setMetadata("Info","xMontage="+(parseInt(xn)+parseInt(xext))+"\nyMontage="+(parseInt(yn)+parseInt(yext))+"\n"); + exit; + } + sc = floor(xstart/(w/xn)); + tc = floor(x/(w/xn)); + sr = floor(ystart/(h/yn)); + tr = floor(y/(h/yn)); + swap(sc,sr,tc,tr); + + } +} + +macro "Annotation Tool - C700 T2709A T8709B T1f09C T8f09D" { + xn = info("xMontage"); + yn = info("yMontage"); + + getCursorLoc(x, y, z, flags); + iw = getWidth/xn; + ih = getHeight/yn; + + co = floor(x/iw); + li = floor(y/ih); + + fontsize = ih/10; + if (fontsize<12) fontsize=12; + marque = substring(str,n,n+1); + if (lcas==1) marque= toLowerCase(marque); + opt=""; + + if (pos == "Clicked quadrant") { + xoffset=0.05; yoffset=0.05; + if (x>((co+0.5)*iw)) xoffset=0.90; + if (y<((li+0.5)*ih)) yoffset=0.85; + } + + if (antialiasedLabels==true) opt=opt+"antialiased"; + setFont("SanSerif",fontsize, opt); + fg = getValue("rgb.foreground"); + makeText(marque ,co*iw+xoffset*iw,(li+1)*ih-yoffset*ih-getValue("font.height")); + Roi.setStrokeColor(fg&0xff0000>>16,fg&0x00ff00>>8,fg&0x0000ff); + Overlay.addSelection("",0); + run("Select None"); + n++; if (n>lengthOf(str)) n=0; +} + +macro "Annotation Tool Options" { + if (nImages>0) setupUndo; + Dialog.create("Annotation - Options"); + Dialog.addString("Labels",str); + Dialog.addCheckbox("Lowercase labels",lcas); + Dialog.addCheckbox("Reset label counter",true); + Dialog.addCheckbox("Antialiased",true); + Dialog.addChoice("Position",newArray("Clicked quadrant","Lower left","Lower right","Upper right","Upper left"),pos); + Dialog.show; + str = Dialog.getString; + lcas = Dialog.getCheckbox; + resetCounter = Dialog.getCheckbox; + if (resetCounter==true) n=0; + antialiasedLabels = Dialog.getCheckbox; + pos=Dialog.getChoice(); + if (pos=="Lower left") {xoffset=0.05; yoffset=0.05;} + else if (pos=="Lower right") {xoffset=0.90; yoffset=0.05;} + else if (pos=="Upper left") {xoffset=0.05; yoffset=0.85;} + else if (pos=="Upper right") {xoffset=0.90; yoffset=0.85;} + +} + +macro "Montage Sync Tool - C800L07f7L707fG" { + w=getWidth; + h= getHeight; + getCursorLoc(x,y,z,flags); + xn = info("xMontage"); + yn = info("yMontage"); + if ((xn==0)||(yn==0)) { + run("Set Montage Layout"); + exit; + } + xc = floor(x/(w/xn)); + yc = floor(y/(h/yn)); + x0 = x-xc*w/xn; + y0 = y-yc*h/yn; + np = 1*xn*yn; + xp =newArray(np); + yp =newArray(np); + for (i=0;i1) { + xa[0]=x0; + ya[0]=y0; + xa[xa.length-1]=x1; + ya[ya.length-1]=y1; + } + makeSelection("freeline",xa,ya); +} + +macro "Add Panel to Manager [F1]" { + roiManager("Add"); + setOption("Show All",true); +} + +macro "Montage to Stack" { + columns = info("xMontage"); + rows = info("yMontage"); + if (rows==0 || columns==0) { + run("Set Montage Layout"); + columns = info("xMontage"); + rows = info("yMontage"); + } + run("Montage to Stack...", "columns=&columns rows=&rows"); +} + +macro "Selected panels to stack [F2]" { + id=getImageID; + t=getTitle; + selectImage(id); + roiManager("select",0); + getSelectionBounds(x,y,sw,sh); + setBatchMode(true); + newImage("Extracted Panels of "+t, "RGB", sw,sh,roiManager("count")); + id2=getImageID; + setPasteMode("copy"); + for (i=0;iffp) { + run("Size...", "width="+sw+" height="+sw/ffc+" constrain interpolate"); + run("Canvas Size...", "width="+sw+" height="+sh+" position=Center zero"); + } else { + run("Size...", "width="+sh*ffc+" height="+sh+" constrain interpolate"); + run("Canvas Size...", "width="+sw+" height="+sh+" position=Center zero"); + } + run("Copy"); + close; + selectImage(id); + setBatchMode(false); + setPasteMode("Copy"); + run("Paste"); +} + +macro "Fill Panel with Clipboard content [F5]" { + getSelectionBounds(x,y,sw,sh); + id=getImageID; + setBatchMode(true); + ffp=sw/sh; + run("Internal Clipboard"); + run("RGB Color"); + ffc=getWidth/getHeight; + if (ffc>ffp) { + run("Size...", "width="+sw*ffc+" height="+sh+" constrain interpolate"); + run("Canvas Size...", "width="+sw+" height="+sh+" position=Center zero"); + } else { + run("Size...", "width="+sw+" height="+sh/ffc+" constrain interpolate"); + run("Canvas Size...", "width="+sw+" height="+sh+" position=Center zero"); + } + run("Copy"); + close; + selectImage(id); + setBatchMode(false); + setPasteMode("Copy"); + run("Paste"); + +} + +macro "Set Montage Layout" { + columns = info("xMontage"); + rows = info("yMontage"); + if (columns>0 && rows>0) + exit("Layout ("+columns+"x"+rows+") is already set"); + Dialog.create("Set Montage Layout"); + Dialog.addNumber("Width:", 2); + Dialog.addNumber("Height:", 2); + Dialog.show; + mw = Dialog.getNumber; + mh = Dialog.getNumber; + setMetadata("Info","xMontage="+mw+"\nyMontage="+mh+"\n"); +} + +macro "Change Montage Layout" { + columns = info("xMontage"); + rows = info("yMontage"); + if (rows==0 || columns==0) { + run("Set Montage Layout"); + columns = info("xMontage"); + rows = info("yMontage"); + } + id1 = getImageID; + title = getTitle; + getVoxelSize(xp,yp,zp,unit); + Dialog.create("Change Montage Layout"); + Dialog.addNumber("Columns:", columns); + Dialog.addNumber("Rows:", rows); + Dialog.show; + columns2 = Dialog.getNumber; + rows2 = Dialog.getNumber; + run("Montage to Stack...", "columns=&columns rows=&rows"); + id2 = getImageID; + run("Make Montage...", "columns=&columns2 rows=&rows2 scale=1.0"); + rename(title); + setVoxelSize(xp,yp,zp,unit); + selectImage(id1); close; + selectImage(id2); close; +} + diff --git a/macros/MeasureStack.txt b/macros/MeasureStack.txt new file mode 100644 index 0000000..78fcf5d --- /dev/null +++ b/macros/MeasureStack.txt @@ -0,0 +1,121 @@ +// This macro runs "Measure" on a all the slices in a stack. With a +// hyperstack, it runs "Measure" in a user-selected (c,t,z) order. +// Unchecking "Channels", "Slices" or "Frames" will select the +// current channel, slice or frame while running "Measure". +// +// Ved P. Sharma, March 21, 2012 +// Albert Einstein College of Medicine, New York + + saveSettings; + setOption("Stack position", true); + if (!Stack.isHyperstack) { + getVoxelSize(width, height, depth, unit); + for (n=1; n<=nSlices; n++) { + setSlice(n); + run("Measure"); + } + if (unit!="pixels") { + depths = newArray(Table.size); + for (i=0; i 0 ) { + getCursorLoc( x, y, z, flags ); + Overlay.getBounds( id, bx, by, bw, bh ); + if( id < 0 ) break; + Overlay.moveSelection( id, x - bw / 2, y - bh / 2 ); + Overlay.getBounds( id, bx, by, bw, bh ); + d = newArray( bx, bx + bw / 2, bx + bw, by, by + bw / 2, by + bh ); + while( Overlay.size > n ) Overlay.removeSelection( Overlay.size - 1 ); + hit = 0x00; + for( i = 0; i < r.length; i = i + 6 ) { // each element + for( p = 0; p < 6; p ++ ) // each remarkable point + if( abs( d [ p ]- r [ i + p ] )< 5 ) { + hit = hit |( 0x01 << p ); // allows for multiple hits + } + if( hit > 0 ) { + ihit = i; + break; + } + } + for( i = 0; i < 6; i ++ ) { + if( hit &( 1 << i )> 0 ) { + if( i < 3 ) { + Overlay.getBounds( id, bx, by, bw, bh ); + Overlay.drawLine( r [ ihit + i ], 0, r [ ihit + i ], getHeight ); + if( i == 0 ) Overlay.moveSelection( id, r [ ihit + i ], by ); + if( i == 1 ) Overlay.moveSelection( id, r [ ihit + i ]- bw / 2, by ); + if( i == 2 ) Overlay.moveSelection( id, r [ ihit + i ]- bw, by ); + } + if( i >= 3 ) { + Overlay.getBounds( id, bx, by, bw, bh ); + Overlay.drawLine( 0, r [ ihit + i ], getWidth, r [ ihit + i ] ); + if( i == 3 ) Overlay.moveSelection( id, bx, r [ ihit + i ] ); + if( i == 4 ) Overlay.moveSelection( id, bx, r [ ihit + i ]- bh / 2 ); + if( i == 5 ) Overlay.moveSelection( id, bx, r [ ihit + i ]- bh ); + } + } + } + Overlay.show; + wait( 20 ); + } + while( Overlay.size > n ) Overlay.removeSelection( Overlay.size - 1 ); +} +// a tool to select one or more overlay elements +// selection is stored in preferences +// click outside a roi to deselect all +// a menu tool to align selected overlay elements + +macro "Select Overlay Tool - Ce00R22fbC0e0o0044C037L77ffL777aL77a7" { + getCursorLoc( x, y, z, flags ); + Overlay.removeRois( 'ToolSelectedOverlayElement' ); + Overlay.show; + n = Overlay.size; + if( n < 1 ) exit( "Overlay required" ); + id = Overlay.indexAt( x, y ); + if( id < 0 ) { + selectNone( ); + call( 'ij.Prefs.set', 'overlaytoolset.selected', '' ); + exit( ); + } + if( flags & 1 > 0 ) { + Overlay.activateSelection( id ); + if( Roi.getName != 'ToolSelectedOverlayElement' ) + selectElement( id, true ); + } else { + Overlay.activateSelection( id ); + if( Roi.getName != 'ToolSelectedOverlayElement' ) + selectElement( id, false ); + } + run( "Select None" ); + highlightSelectedROIs( ); +} +function highlightSelectedROIs( ) { + Overlay.removeRois( 'ToolSelectedOverlayElement' ); + selected = getSelectedElements( ); + s = split( selected, ',' ); + //print( selected ); + for( i = 0; i < s.length; i ++ ) { + id = s [ i ]; + Overlay.getBounds( id, bx, by, bw, bh ); + makeRectangle( bx, by, bw, bh ); + Roi.setName( 'ToolSelectedOverlayElement' ); + Overlay.addSelection( '#90ff0000', 3 ); + } + run( "Select None" ); + Overlay.show; +} +function selectNone( ) { + Overlay.removeRois( 'ToolSelectedOverlayElement' ); + Overlay.show; + //call( 'ij.Prefs.set', 'overlaytoolset.selected', '' ); +} +function selectElement( id, add ) { + if( add == true ) { + selected = getSelectedElements( ); + s = split( selected, ',' ); + isSelected = false; + for( i = 0; i < s.length; i ++ ) { + if( 1 * s [ i ]== id ) isSelected = true; + } + if( ! isSelected ) { + call( 'ij.Prefs.set', 'overlaytoolset.selected', selected + ',' + id ); + selected = getSelectedElements( ); + } else { + unselectElement( id ); + } + } else { + call( 'ij.Prefs.set', 'overlaytoolset.selected', id ); + } + highlightSelectedROIs( ); +} +function unselectElement( id ) { + selected = getSelectedElements( ); + s = split( selected, ',' ); + selected = ''; + for( i = 0; i < s.length; i ++ ) { + if( s [ i ]!= id ) selected = selected + s [ i ]+ ','; + } + call( 'ij.Prefs.set', 'overlaytoolset.selected', selected ); + selected = getSelectedElements( ); + highlightSelectedROIs( ); + run( "Select None" ); +} +function getSelectedElements( ) { + selected = call( 'ij.Prefs.get', 'overlaytoolset.selected', '' ); + while( selected.endsWith( "," ) ) selected = substring( selected, 0, lengthOf( selected )- 1 ); + while( selected.startsWith( "," ) ) selected = substring( selected, 1, lengthOf( selected ) ); + return selected; +} +var dCmds = newMenu( "Align Overlay Menu Tool", + newArray( "Top", "Middle", "Bottom", "-", "Left", "Center", "Right" ) ); +// a menu tool to align selected ROIs +macro "Align Overlay Menu Tool - C037L00f0R2244R8248" { + cmd = getArgument( ); + if( cmd == "Top" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + tops = newArray( s.length ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + tops [ i ]= y; + } + //Array.print( tops ); + Array.getStatistics( tops, min, max, mean, stdDev ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], x, min ); + } + Overlay.show( ); + } else if( cmd == "Bottom" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + tops = newArray( s.length ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + tops [ i ]= y + h; + } + Array.getStatistics( tops, min, max, mean, stdDev ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], x, max - h ); + } + selectNone( ); + } else if( cmd == "Left" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + tops = newArray( s.length ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + tops [ i ]= x; + } + Array.getStatistics( tops, min, max, mean, stdDev ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], min, y ); + } + selectNone( ); + } else if( cmd == "Right" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + tops = newArray( s.length ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + tops [ i ]= x + w; + } + Array.getStatistics( tops, min, max, mean, stdDev ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], max - w, y ); + } + selectNone( ); + } else if( cmd == "Middle" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + Overlay.getBounds( s [ 0 ], x, y, w, h ); + middle = y + h / 2; + for( i = 0; i < s.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], x, middle - h / 2 ); + } + selectNone( ); + } else if( cmd == "Center" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + Overlay.getBounds( s [ 0 ], x, y, w, h ); + center = x + w / 2; + for( i = 0; i < s.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], center - w / 2, y ); + } + selectNone( ); + } + highlightSelectedROIs( ); +} +// Click to delete Overlay element +// A confirm dialog is shown +// you can choose not to show it. +macro "Delete Overlay Tool - C037R00ddB58Cd00L0088L0880" { + getCursorLoc( x, y, z, flags ); + selectNone( ); + call( 'ij.Prefs.set', 'overlaytoolset.selected', '' ); + id = Overlay.indexAt( x, y ); + if( id != - 1 ) { + showWarning = call( "ij.Prefs.get", "overlaytoolset.deletewarning", true ); + mustDelete = true; + if( showWarning == true ) { + Dialog.create( "Delete ROI tool options" ); + Dialog.addCheckbox( "Delete this ROI", true ); + Dialog.addCheckbox( "Show this dialog", showWarning ); + Dialog.show( ); + mustDelete = Dialog.getCheckbox( ); + showWarning = Dialog.getCheckbox( ); + call( "ij.Prefs.set", "overlaytoolset.deletewarning", showWarning ); + } + if( mustDelete ) Overlay.removeSelection( id ); + } +} +macro "Overlay Toolset Help Action Tool - C037T3f18?" { + html = "" + + "Toolset Description
" + + "

* Move overlay tool" + + "
Click and drag an overlay element;" + + "
It snaps to alignment guides to other elements.

" + + "

* Overlay select tool" + + "
Click an overlay element to select it." + + "
Shift-click to add elements to the selection." + + "
Shift-click a selected element to deselect it.

" + + "

* Overlay align tool menu" + + "
Use this menu to align selected elements

" + + "

* Overlay delete tool" + + "
Click to remove an overlay element

" + + "

* Overlay toolset help action tool" + + "
This dialog. You can leave it open to try toolset functions.

" + + ""; + Dialog.createNonBlocking( "Overlay Toolset Help" ); + Dialog.addMessage( html ); + Dialog.show( ); +} diff --git a/macros/Plugin_Frame.src b/macros/Plugin_Frame.src new file mode 100644 index 0000000..f31fc1a --- /dev/null +++ b/macros/Plugin_Frame.src @@ -0,0 +1,18 @@ +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.plugin.frame.*; + +public class Plugin_Frame extends PlugInFrame { + + public Plugin_Frame() { + super("Plugin_Frame"); + TextArea ta = new TextArea(15, 50); + add(ta); + pack(); + GUI.center(this); + show(); + } + +} diff --git a/macros/Prototype_Tool.src b/macros/Prototype_Tool.src new file mode 100644 index 0000000..7cf2850 --- /dev/null +++ b/macros/Prototype_Tool.src @@ -0,0 +1,24 @@ +// Prototype plugin tool. There are more plugin tools at +// http://imagej.nih.gov/ij/plugins/index.html#tools +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.plugin.tool.PlugInTool; +import java.awt.event.*; + +public class Prototype_Tool extends PlugInTool { + + public void mousePressed(ImagePlus imp, MouseEvent e) { + IJ.log("mouse pressed: "+e); + } + + public void mouseDragged(ImagePlus imp, MouseEvent e) { + IJ.log("mouse dragged: "+e); + } + + public void showOptionsDialog() { + IJ.log("icon double-clicked"); + } + +} diff --git a/macros/RoiMenuTool.txt b/macros/RoiMenuTool.txt new file mode 100644 index 0000000..2b3699f --- /dev/null +++ b/macros/RoiMenuTool.txt @@ -0,0 +1,88 @@ + var rCmds = newMenu("ROI Menu Tool", + newArray("Set Default Group...", "Set Default Stroke Width...", "-", + "Set Group of Selected ROIs", "Select Group", "-", "Properties..." , "Install Keypad Shortcuts") ); + + macro "ROI Menu Tool - C037T0d15RT8c12oTfc12i" { + cmd = getArgument(); + if (cmd=="Set Default Group...") + setDefaultRoiGroup(); + else if (cmd=="Set Default Stroke Width...") + setDefaultRoiStrokeWidth(); + else if (cmd=="Set Group of Selected ROIs") + setRoiGroup(); + else if (cmd=="Select Group") + selectRoiGroup(); + else if (cmd=="Properties...") + properties(); + else if (cmd=="Install Keypad Shortcuts") + call("ij.plugin.MacroInstaller.installFromJar", "/macros/RoiMenuTool.txt+"); + } + + // Numeric keypad shortcuts used to set the default ROI group + macro "Keypad shortcuts for setting default group" { } + macro "Group 0 (none) [n0]" { npad(0); } + macro "Group 1 [n1]" { npad(1); } + macro "Group 2 [n2]" { npad(2); } + macro "Group 3 [n3]" { npad(3); } + macro "Group 4 [n4]" { npad(4); } + macro "Group 5 [n5]" { npad(5); } + macro "Group 6 [n6]" { npad(6); } + macro "Group 7 [n7]" { npad(7); } + macro "Group 8 [n8]" { npad(8); } + macro "Group 9 [n9]" { npad(9); } + + function npad(digit) { + Roi.setDefaultGroup(digit); + } + + function properties() { + if (selectionType==-1) { + showMessage("Selection required"); + exit; + } + run("Properties... "); + } + + function setDefaultRoiGroup() { + group = Roi.getDefaultGroup; + Dialog.create("Set Default Group"); + Dialog.addNumber("Default group", group); + Dialog.show; + group = Dialog.getNumber(); + Roi.setDefaultGroup(group); + call("ij.plugin.frame.Recorder.recordString", "Roi.setDefaultGroup("+group+");\n"); + } + + function setDefaultRoiStrokeWidth() { + width = Roi.getDefaultStrokeWidth; + Dialog.create("Set Default Stroke Width"); + Dialog.addNumber("Default stroke width", width); + Dialog.show; + width = Dialog.getNumber(); + Roi.setDefaultStrokeWidth(width); + call("ij.plugin.frame.Recorder.recordString", "Roi.setDefaultStrokeWidth("+width+");\n"); + } + + function setRoiGroup() { + Dialog.create("Set Group"); + Dialog.addString("Group", "1"); + Dialog.show; + group = Dialog.getString(); + RoiManager.setGroup(group); + if (call("ij.plugin.frame.Recorder.scriptMode")=="true") + call("ij.plugin.frame.Recorder.recordString", "rm.setGroup("+group+");\n"); + else + call("ij.plugin.frame.Recorder.recordString", "RoiManager.setGroup("+group+");\n"); + } + + function selectRoiGroup() { + Dialog.create("Select group"); + Dialog.addString("Group", "0"); + Dialog.show; + group = Dialog.getString(); + RoiManager.selectGroup(group); + if (call("ij.plugin.frame.Recorder.scriptMode")=="true") + call("ij.plugin.frame.Recorder.recordString", "rm.selectGroup("+group+");\n"); + else + call("ij.plugin.frame.Recorder.recordString", "RoiManager.selectGroup("+group+");\n"); + } diff --git a/macros/Search.txt b/macros/Search.txt new file mode 100644 index 0000000..2cf005b --- /dev/null +++ b/macros/Search.txt @@ -0,0 +1,139 @@ +// "Search" +// This macro searches for text in files contained in a directory. +// TF, 2011.02 Added support for scripts; Recordable. + + str = ""; + contents = true; + ignore = false; + search = "Macros"; + firstLine = true; + arg = getArgument; + if (arg!="") { + args = split(arg, "|"); + if (args.length==4) { + str = args[0]; + contents = parseInt(args[1]); + ignore = parseInt(args[2]); + search = args[3]; + } + } + extensions = newArray(".java", ".txt", ".ijm", ".js", ".py", ".rb", ".clj", ".bsh", ".html"); + IJdir = getDirectory("imagej"); + + Dialog.create("Search"); + Dialog.addString("_", str, 20); + items = newArray("Macros", "Scripts", "Java", "ImageJ folder", "Choose..."); + Dialog.setInsets(2,20,0); + Dialog.addRadioButtonGroup("Search:", items, 5, 1, search); + Dialog.setInsets(0, 20, 0); + Dialog.addCheckbox("Search_contents", contents); + Dialog.addCheckbox("Ignore case", ignore); + Dialog.setInsets(10, 0, 0); + Dialog.addMessage("In the Log window, to open a file,\ndouble-click on its file path."); + Dialog.show(); + str = Dialog.getString(); + contents = Dialog.getCheckbox(); + ignore = Dialog.getCheckbox(); + search = Dialog.getRadioButton(); + if (str=="") + exit("Search string is empty"); + + sourceExists = File.exists(IJdir+"source"); + searchNames = false; + dir1=""; dir2=""; dir3=""; + if (search=="Scripts") { + dir1 = getDirectory("macros"); + dir2 = getDirectory("plugins"); + dir3 = IJdir+"scripts/"; + extensions = newArray(".js", ".py", ".rb", ".clj", ".bsh"); + } else if (search=="Java") { + dir1 = getDirectory("plugins"); + if (sourceExists) + dir2 = IJdir+"source"+"/"; + extensions = newArray(".java"); + } else if (search=="ImageJ folder") { + dir1 = getDirectory("imagej"); + searchNames = true; + } else if (search=="Choose...") { + dir1 = getDirectory("Choose a Directory"); + searchNames = true; + } else { + dir1 = getDirectory("macros"); + dir2 = getDirectory("plugins"); + extensions = newArray(".txt", ".ijm"); + } + if (ignore) + str = toLowerCase(str); + count = 0; + if (dir1!="") find(dir1); + if (dir2!="") find(dir2); + if (dir3!="") find(dir3); + if (indexOf(str, "|")==-1) + return ""+str+"|"+contents+"|"+ignore+"|"+search; + exit; + + function find(dir) { + list = getFileList(dir); + for (i=0; iLookup Tables menu. + +saveSettings(); +list = getList("LUTs"); +setBatchMode(true); +newImage("ramp", "8-bit Ramp", 256, 32, 1); +newImage("luts", "RGB White", 256, 48, 1); +count = 0; +setForegroundColor(255, 255, 255); +setBackgroundColor(255, 255, 255); +setFont("SansSerif", 12,"antialiased"); +for (i=0; i 1 px"); +if (selectionType != 4) + exit("No traced selection found"); +getThreshold(lowThr, hiThr); +getSelectionCoordinates(xx, yy); +len = xx.length; +polyX = newArray(len); +polyY = newArray(len); +count = 0; +for(jj = 0; jj < len; jj++) { + x1 = xx[jj]; + y1 = yy[jj]; + x2 = xx[(jj+1)%len]; + y2 = yy[(jj+1)%len]; + dd = 1; + if (y1 == y2) {//horizontal separator + if (x1 > x2) dd = -1; + for(x = x1; x != x2; x+= dd){ + processPixelPair(x, y1, x+dd, y1); + } + } + else {//vertical separator + if (y1 > y2) dd = -1; + for(y = y1; y != y2; y+= dd){ + processPixelPair(x1, y, x1, y+dd); + } + } +} +polyX = Array.trim(polyX, count); +polyY = Array.trim(polyY, count); +makeSelection("polygon", polyX, polyY); +run("Interpolate", "interval=1 adjust"); //after button released + +//processes neighbors of this separator line and adds vertex +function processPixelPair(x1, y1, x2, y2) { + if (x1 == x2) { + val1 = getPixel(x1, minOf(y1, y2)); + val2 = getPixel(x1 - 1, minOf(y1, y2)); + } + if (y1 == y2){ + val1 = getPixel(minOf(x1, x2), y1); + val2 = getPixel(minOf(x1, x2), y1 -1); + } + bright = maxOf(val1, val2); + dark = minOf(val1, val2); + + if (bright>=lowThr && dark<=lowThr) + thr = lowThr; + if (bright>=hiThr && dark<=hiThr) + thr = hiThr; + if (dark==bright) + fraction = 0.5; + else + fraction = (thr - dark)/(bright - dark); + if (val1 < val2) + fraction = 1 - fraction; + if (y1 == y2) { + newY = minOf(y1, y2) + fraction - 0.5; + newX = (x1 + x2)/2; + } + if (x1 == x2) { + newX = minOf(x1, x2) + fraction - 0.5; + newY = (y1 + y2)/2; + } + polyX[count] = newX; + polyY[count] = newY; + count++; + if (count == polyX.length){ + polyX = Array.concat(polyX, polyX); + polyY = Array.concat(polyY, polyY); + } +} diff --git a/macros/SprayCanTool.txt b/macros/SprayCanTool.txt new file mode 100644 index 0000000..8ed59b9 --- /dev/null +++ b/macros/SprayCanTool.txt @@ -0,0 +1,41 @@ +// Spray Can Tool + + var width=100, dotSize=1, rate=6; + + macro 'Spray Can Tool - C123D20D22D24D41D43D62D82Da2C037L93b3D84Dc4L75d5L757f Ld5dfLa7d7LabdbLa9d9LacdcLa7ac' { + setLineWidth(dotSize); + radius=width/2; radius2=radius*radius; + start = getTime(); + autoUpdate(false); + n = 25*exp(0.9*(10-rate)); + if (n<=5) n = 0; + while (true) { + getCursorLoc(x, y, z, flags); + if (flags&16==0) exit(); + x2 = (random()-0.5)*width; + y2 = (random()-0.5)*width; + if (x2*x2+y2*y2start+50) { + updateDisplay(); + start = getTime(); + } + } + for (i=0; i10) rate = 10; + } diff --git a/macros/StacksMenuTool.txt b/macros/StacksMenuTool.txt new file mode 100644 index 0000000..636dc6e --- /dev/null +++ b/macros/StacksMenuTool.txt @@ -0,0 +1,15 @@ + var sCmds = newMenu("Stacks Menu Tool", + newArray("Add Slice", "Delete Slice", "Set Slice...", + "-", "3D Project...", "Grouped Z Project...", "Z Project...", "Orthogonal Views", "Plot Z-axis Profile", "Reslice [/]...", + "-", "Images to Stack", "Stack to Images", "Make Montage...", + "-", "Make Substack...", "Stack to Hyperstack...", + "-", "Combine...", "Concatenate...", "Flip Z", "Label...", "Animation Options...", + "-", "T1 Head (2.4M, 16-bits)")); + + macro "Stacks Menu Tool - C037T0b11ST8b09tTcb09k" { + cmd = getArgument(); + if (cmd=="Images to Stack") + run(cmd, " "); + else if (cmd!="-") + run(cmd); + } diff --git a/macros/StartupMacros.txt b/macros/StartupMacros.txt new file mode 100644 index 0000000..4e7059d --- /dev/null +++ b/macros/StartupMacros.txt @@ -0,0 +1,7 @@ +// Default startup macros + + macro "Developer Menu Built-in Tool" {} + macro "Brush Built-in Tool" {} + macro "Flood Filler Built-in Tool" {} + macro "Arrow Built-in Tool" {} + diff --git a/macros/TimeStamp.ijm b/macros/TimeStamp.ijm new file mode 100644 index 0000000..9f8f915 --- /dev/null +++ b/macros/TimeStamp.ijm @@ -0,0 +1,13 @@ +x=20; y=30; size=18; +interval=1; //seconds +i = floor(i*interval); // 'i' is the image index +setFont("SansSerif", size, "antialiased"); +setColor("white"); +s = ""+pad(floor(i/3600))+":"+pad(floor((i/60)%60))+":"+pad(i%60); +drawString(s, x, y); +function pad(n) { + str = toString(n); + if (lengthOf(str)==1) str="0"+str; + return str; +} + diff --git a/plugins/Decarburization_Measurements.jar b/plugins/Decarburization_Measurements.jar new file mode 100644 index 0000000..0bf116d Binary files /dev/null and b/plugins/Decarburization_Measurements.jar differ diff --git a/plugins/MacAdapter.class b/plugins/MacAdapter.class new file mode 100644 index 0000000..ab5202e Binary files /dev/null and b/plugins/MacAdapter.class differ diff --git a/plugins/MacAdapter.source b/plugins/MacAdapter.source new file mode 100644 index 0000000..7eff785 --- /dev/null +++ b/plugins/MacAdapter.source @@ -0,0 +1,54 @@ +package ij.plugin; +import ij.plugin.*; +import ij.*; +import ij.io.*; +import com.apple.eawt.*; +import java.util.Vector; + +/** This Mac-specific plugin is designed to handle the “About ImageJ", + Preferences and “Quit ImageJ" commands in the ImageJ menu, and to + open files dropped on ImageJ.app and to open double-clicked files + with creator code "imgJ". With Java 8, the “About ImageJ" and + “Quit ImageJ” commands work without MacAdapter. +*/ +public class MacAdapter implements PlugIn, ApplicationListener, Runnable { + static Vector paths = new Vector(); + + public void run(String arg) { + Application app = new Application(); + app.setEnabledPreferencesMenu(true); + app.addApplicationListener(this); + } + + public void handleAbout(ApplicationEvent event) { + IJ.doCommand("About ImageJ..."); + event.setHandled(true); + } + + public void handleOpenFile(ApplicationEvent event) { + paths.add(event.getFilename()); + Thread thread = new Thread(this, "Open"); + thread.setPriority(thread.getPriority()-1); + thread.start(); + } + + public void handlePreferences(ApplicationEvent event) { + IJ.error("The ImageJ preferences are in the Edit>Options menu."); + } + + public void handleQuit(ApplicationEvent event) { + new Executer("Quit", null); // works with the CommandListener + //IJ.getInstance().quit(); + } + + public void run() { + if (paths.size() > 0) { + (new Opener()).openAndAddToRecent((String) paths.remove(0)); + } + } + + public void handleOpenApplication(ApplicationEvent event) {} + public void handleReOpenApplication(ApplicationEvent event) {} + public void handlePrintFile(ApplicationEvent event) {} + +} diff --git a/plugins/MacAdapter9.class b/plugins/MacAdapter9.class new file mode 100644 index 0000000..44ef7dd Binary files /dev/null and b/plugins/MacAdapter9.class differ diff --git a/plugins/MacAdapter9.source b/plugins/MacAdapter9.source new file mode 100644 index 0000000..d5be1ef --- /dev/null +++ b/plugins/MacAdapter9.source @@ -0,0 +1,58 @@ +package ij.plugin; +import ij.*; +import ij.io.*; +import java.awt.Desktop; +import java.awt.desktop.*; +import java.io.File; +import java.util.Vector; + +/** This Mac-specific plugin is designed to handle the "About ImageJ" + * command in the ImageJ menu, to open files dropped on ImageJ.app + * and to open double-clicked files with creator code "imgJ". + * With Java 9 or newer, we use java.awt.desktop instead of the + * previous com.apple.eawt.* classes. + * @author Alan Brooks +*/ +public class MacAdapter9 implements PlugIn, AboutHandler, OpenFilesHandler, QuitHandler, Runnable { + static Vector paths = new Vector(); + + public void run(String arg) { + Desktop dtop = Desktop.getDesktop(); + dtop.setOpenFileHandler(this); + dtop.setAboutHandler(this); + dtop.setQuitHandler(this); + } + + @Override + public void handleAbout(AboutEvent e) { + IJ.doCommand("About ImageJ..."); + } + + @Override + public void openFiles(OpenFilesEvent e) { + for (File file: e.getFiles()) { + paths.add(file.getPath()); + Thread thread = new Thread(this, "Open"); + thread.setPriority(thread.getPriority()-1); + thread.start(); + } + } + + @Override + public void handleQuitRequestWith(QuitEvent e, QuitResponse response) { + new Executer("Quit", null); // works with the CommandListener + } + + // Not adding preference handling + // because we don't have the equivalent of app.setEnabledPreferencesMenu(true); + // @Override + // public void handlePreferences(PreferencesEvent e) { + // IJ.error("The ImageJ preferences are in the Edit>Options menu."); + // } + + public void run() { + if (paths.size() > 0) { + (new Opener()).openAndAddToRecent(paths.remove(0)); + } + } +} diff --git a/plugins/TESTPlugin_.jar b/plugins/TESTPlugin_.jar new file mode 100644 index 0000000..8436d7e Binary files /dev/null and b/plugins/TESTPlugin_.jar differ diff --git a/src/ij/CommandListener.java b/src/ij/CommandListener.java new file mode 100644 index 0000000..7ec2870 --- /dev/null +++ b/src/ij/CommandListener.java @@ -0,0 +1,16 @@ +package ij; + + /** Plugins that implement this interface are notified when ImageJ + is about to run a menu command. There is an example plugin at + http://imagej.nih.gov/ij/plugins/download/misc/Command_Listener.java + */ + public interface CommandListener { + + /* The method is called when ImageJ is about to run a menu command, + where 'command' is the name of the command. Return this string + and ImageJ will run the command, return a different command name + and ImageJ will run that command, or return null to not run a command. + */ + public String commandExecuting(String command); + +} diff --git a/src/ij/CompositeImage.java b/src/ij/CompositeImage.java new file mode 100644 index 0000000..b0a2cc3 --- /dev/null +++ b/src/ij/CompositeImage.java @@ -0,0 +1,666 @@ +package ij; +import ij.process.*; +import ij.gui.*; +import ij.plugin.*; +import ij.plugin.frame.*; +import ij.io.FileInfo; +import java.awt.*; +import java.awt.image.*; + +public class CompositeImage extends ImagePlus { + + /** Display modes (note: TRANSPARENT mode has not yet been implemented) */ + public static final int COMPOSITE=1, COLOR=2, GRAYSCALE=3, TRANSPARENT=4; + public static final int MAX_CHANNELS = 7; + int[] rgbPixels; + boolean newPixels; + MemoryImageSource imageSource; + Image awtImage; + WritableRaster rgbRaster; + SampleModel rgbSampleModel; + BufferedImage rgbImage; + ColorModel rgbCM; + ImageProcessor[] cip; + Color[] colors = {Color.red, Color.green, Color.blue, Color.white, Color.cyan, Color.magenta, Color.yellow}; + LUT[] lut; + int currentChannel = -1; + int previousChannel; + int currentSlice = 1; + int currentFrame = 1; + boolean singleChannel; + boolean[] active = new boolean[MAX_CHANNELS]; + int mode = COLOR; + int bitDepth; + double[] displayRanges; + byte[][] channelLuts; + boolean customLuts; + boolean syncChannels; + + public CompositeImage(ImagePlus imp) { + this(imp, COLOR); + } + + public CompositeImage(ImagePlus imp, int mode) { + if (modeGRAYSCALE) + mode = COLOR; + this.mode = mode; + int channels = imp.getNChannels(); + bitDepth = getBitDepth(); + if (IJ.debugMode) IJ.log("CompositeImage: "+imp+" "+mode+" "+channels); + ImageStack stack2; + boolean isRGB = imp.getBitDepth()==24; + if (isRGB) { + if (imp.getImageStackSize()>1) + throw new IllegalArgumentException("RGB stacks not supported"); + stack2 = getRGBStack(imp); + } else + stack2 = imp.getImageStack(); + int stackSize = stack2.getSize(); + if (channels==1 && isRGB) + channels = 3; + if (channels==1 && stackSize<=MAX_CHANNELS && !imp.dimensionsSet) + channels = stackSize; + if (channels<1 || (stackSize%channels)!=0) + throw new IllegalArgumentException("stacksize not multiple of channels"); + if (mode==COMPOSITE && channels>MAX_CHANNELS) + this.mode = COLOR; + compositeImage = true; + int z = imp.getNSlices(); + int t = imp.getNFrames(); + if (channels==stackSize || channels*z*t!=stackSize) + setDimensions(channels, stackSize/channels, 1); + else + setDimensions(channels, z, t); + setStack(imp.getTitle(), stack2); + setCalibration(imp.getCalibration()); + FileInfo fi = imp.getOriginalFileInfo(); + if (fi!=null) { + displayRanges = fi.displayRanges; + channelLuts = fi.channelLuts; + } + setFileInfo(fi); + Object info = imp.getProperty("Info"); + if (info!=null) + setProperty("Info", imp.getProperty("Info")); + setProperties(imp.getPropertiesAsArray()); + if (mode==COMPOSITE) { + for (int i=0; i0 && (stack2.getProcessor(1) instanceof ColorProcessor)) { // RGB? + cip = null; + lut = null; + return; + } + setupLuts(channels); + if (mode==COMPOSITE) { + cip = new ImageProcessor[channels]; + for (int i=0; iMAX_CHANNELS?createLutFromColor(Color.white):null; + for (int i=0; istack2.getSize() || channels>MAX_CHANNELS) + return; + for (int i=0; inChannels) ch = nChannels; + boolean newChannel = false; + if (ch-1!=currentChannel) { + previousChannel = currentChannel; + currentChannel = ch-1; + newChannel = true; + } + + ImageProcessor ip = getProcessor(); + if (mode!=COMPOSITE) { + if (newChannel) { + setupLuts(nChannels); + LUT cm = lut[currentChannel]; + if (ip!=null && !(ip instanceof ColorProcessor)) { + if (mode==COLOR) + ip.setLut(cm); + if (!(cm.min==0.0&&cm.max==0.0)) + ip.setMinAndMax(cm.min, cm.max); + } + if (!IJ.isMacro()) ContrastAdjuster.update(); + for (int i=0; i=nChannels) { + setSlice(1); + currentChannel = 0; + newChannel = true; + } + bitDepth = getBitDepth(); + } + + if (newChannel) { + getProcessor().setMinAndMax(cip[currentChannel].getMin(), cip[currentChannel].getMax()); + if (!IJ.isMacro()) ContrastAdjuster.update(); + } + //IJ.log(nChannels+" "+ch+" "+currentChannel+" "+newChannel); + + if (getSlice()!=currentSlice || getFrame()!=currentFrame) { + currentSlice = getSlice(); + currentFrame = getFrame(); + int position = getStackIndex(1, currentSlice, currentFrame); + if (cip==null) return; + for (int i=0; icip.length) + return; + for (int i=1; icip.length) + return null; + else + return cip[channel-1]; + } + + public boolean[] getActiveChannels() { + return active; + } + + public synchronized void setMode(int mode) { + if (modeGRAYSCALE) + return; + if (mode==COMPOSITE && getNChannels()>MAX_CHANNELS) + mode = COLOR; + for (int i=0; ilut.length) + throw new IllegalArgumentException("Channel out of range: "+channel); + return lut[channel-1]; + } + + /* Returns the LUT used by the current channel. */ + public LUT getChannelLut() { + int c = getChannelIndex(); + return lut[c]; + } + + /* Returns a copy of this image's channel LUTs as an array. */ + public LUT[] getLuts() { + int channels = getNChannels(); + if (lut==null) + setupLuts(channels); + LUT[] luts = new LUT[channels]; + for (int i=0; iMAX_CHANNELS && getMode()==COMPOSITE) + setMode(COLOR); + setup(nChannels, getImageStack()); + } + + public void completeReset() { + cip = null; + lut = null; + } + + /* Sets the LUT of the current channel. */ + public void setChannelLut(LUT table) { + int c = getChannelIndex(); + double min = lut[c].min; + double max = lut[c].max; + lut[c] = table; + lut[c].min = min; + lut[c].max = max; + if (mode==COMPOSITE && cip!=null && clut.length) + throw new IllegalArgumentException("Channel out of range"); + lut[channel-1] = (LUT)table.clone(); + if (getWindow()!=null && channel==getChannel()) + getProcessor().setLut(lut[channel-1]); + if (cip!=null && cip.length>=channel && cip[channel-1]!=null) + cip[channel-1].setLut(lut[channel-1]); + else + cip = null; + customLuts = true; + } + + /* Sets the IndexColorModel of the current channel. */ + public void setChannelColorModel(IndexColorModel cm) { + setChannelLut(new LUT(cm,0.0,0.0)); + } + + public void setDisplayRange(double min, double max) { + ip.setMinAndMax(min, max); + int c = getChannelIndex(); + lut[c].min = min; + lut[c].max = max; + if (getWindow()==null && cip!=null && c0) synchronized (listeners) { + for (int i=0; i0 && command.charAt(len-1)!=']') + IJ.setKeyUp(IJ.ALL_KEYS); // set keys up except for "<", ">", "+" and "-" shortcuts + } catch(Throwable e) { + IJ.showStatus(""); + IJ.showProgress(1, 1); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) imp.unlock(); + String msg = e.getMessage(); + if (e instanceof OutOfMemoryError) + IJ.outOfMemory(command); + else if (e instanceof RuntimeException && msg!=null && msg.equals(Macro.MACRO_CANCELED)) + ; //do nothing + else { + CharArrayWriter caw = new CharArrayWriter(); + PrintWriter pw = new PrintWriter(caw); + e.printStackTrace(pw); + String s = caw.toString(); + if (IJ.isMacintosh()) { + if (s.indexOf("ThreadDeath")>0) + return; + s = Tools.fixNewLines(s); + } + int w=500, h=340; + if (s.indexOf("UnsupportedClassVersionError")!=-1) { + if (s.indexOf("version 49.0")!=-1) { + s = e + "\n \nThis plugin requires Java 1.5 or later."; + w=700; h=150; + } + if (s.indexOf("version 50.0")!=-1) { + s = e + "\n \nThis plugin requires Java 1.6 or later."; + w=700; h=150; + } + if (s.indexOf("version 51.0")!=-1) { + s = e + "\n \nThis plugin requires Java 1.7 or later."; + w=700; h=150; + } + if (s.indexOf("version 52.0")!=-1) { + s = e + "\n \nThis plugin requires Java 1.8 or later."; + w=700; h=150; + } + } + if (IJ.getInstance()!=null) { + s = IJ.getInstance().getInfo()+"\n \n"+s; + new TextWindow("Exception", s, w, h); + } else + IJ.log(s); + } + } finally { + if (thread!=null) + WindowManager.setTempCurrentImage(null); + } + } + + void runCommand(String cmd) { + Hashtable table = Menus.getCommands(); + String className = (String)table.get(cmd); + if (className!=null) { + String arg = ""; + if (className.endsWith("\")")) { + // extract string argument (e.g. className("arg")) + int argStart = className.lastIndexOf("(\""); + if (argStart>0) { + arg = className.substring(argStart+2, className.length()-2); + className = className.substring(0, argStart); + } + } + if (Prefs.nonBlockingFilterDialogs) { + // we have the plugin class name, let us see whether it is allowed to run it + ImagePlus imp = WindowManager.getCurrentImage(); + boolean imageLocked = imp!=null && imp.isLockedByAnotherThread(); + if (imageLocked && !allowedWithLockedImage(className)) { + IJ.beep(); + IJ.showStatus("\""+cmd + "\" blocked because \"" + imp.getTitle() + "\" is locked"); + return; + } + } + // run the plugin + if (IJ.shiftKeyDown() && className.startsWith("ij.plugin.Macro_Runner") && !Menus.getShortcuts().contains("*"+cmd)) + IJ.open(IJ.getDirectory("plugins")+arg); + else + IJ.runPlugIn(cmd, className, arg); + } else { // command is not a plugin + // is command in the Plugins>Macros menu? + if (MacroInstaller.runMacroCommand(cmd)) + return; + // is it in the Image>Lookup Tables menu? + if (loadLut(cmd)) + return; + // is it in the File>Open Recent menu? + if (openRecent(cmd)) + return; + // is it an example in Help>Examples menu? + if (Editor.openExample(cmd)) + return; + if ("Auto Threshold".equals(cmd)&&(String)table.get("Auto Threshold...")!=null) + runCommand("Auto Threshold..."); + else if ("Enhance Local Contrast (CLAHE)".equals(cmd)&&(String)table.get("CLAHE ")!=null) + runCommand("CLAHE "); + else { + if ("Table...".equals(cmd)) + IJ.runPlugIn("ij.plugin.NewPlugin", "table"); + else { + if (repeatingCommand) + IJ.runMacro(previousCommand); + else { + if (!extraCommand(cmd)) + IJ.error("Unrecognized command: \"" + cmd+"\""); + } + } + } + } + } + + private boolean extraCommand(String cmd) { + if (cmd!=null && cmd.equals("Duplicate Image...")) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + Duplicator.ignoreNextSelection(); + IJ.run(imp, "Duplicate...", ""); + } else + IJ.noImage(); + return true; + } else + return false; + } + + /** If the foreground image is locked during a filter operation with NonBlockingGenericDialog, + * the following plugins are allowed */ + boolean allowedWithLockedImage(String className) { + return className.equals("ij.plugin.Zoom") || + className.equals("ij.plugin.frame.ContrastAdjuster") || + className.equals("ij.plugin.SimpleCommands") || //includes Plugins>Utiltites>Reset (needed to reset a locked image) + className.equals("ij.plugin.WindowOrganizer") || + className.equals("ij.plugin.URLOpener"); + } + + /** Opens a .lut file from the ImageJ/luts directory and returns 'true' if successful. */ + public static boolean loadLut(String name) { + String path = IJ.getDirectory("luts")+name.replace(" ","_")+".lut"; + File f = new File(path); + if (!f.exists()) { + path = IJ.getDirectory("luts")+name+".lut"; + f = new File(path); + } + if (!f.exists()) { + path = IJ.getDirectory("luts")+name.toLowerCase().replace(" ","_")+".lut"; + f = new File(path); + } + if (!f.exists() && Character.isLowerCase(name.charAt(0))) { + String name2 = name.substring(0,1).toUpperCase()+name.substring(1); + path = IJ.getDirectory("luts")+name2+".lut"; + f = new File(path); + } + if (!f.exists() && name.toLowerCase().equals("viridis")) { + path = IJ.getDirectory("luts")+"mpl-viridis.lut"; //Fiji version + f = new File(path); + } + if (f.exists()) { + String dir = OpenDialog.getLastDirectory(); + IJ.open(path); + OpenDialog.setLastDirectory(dir); + return true; + } + return false; + } + + /** Opens a file from the File/Open Recent menu + and returns 'true' if successful. */ + boolean openRecent(String cmd) { + Menu menu = Menus.getOpenRecentMenu(); + if (menu==null) return false; + for (int i=0; imacro + on the current thread. Returns any string value returned by + the macro, null if the macro does not return a value, or + "[aborted]" if the macro was aborted due to an error. The + equivalent macro function is eval(). */ + public static String runMacro(String macro) { + return runMacro(macro, ""); + } + + /** Runs the macro contained in the string macro + on the current thread. The optional string argument can be + retrieved in the called macro using the getArgument() macro + function. Returns any string value returned by the macro, null + if the macro does not return a value, or "[aborted]" if the + macro was aborted due to an error. */ + public static String runMacro(String macro, String arg) { + Macro_Runner mr = new Macro_Runner(); + return mr.runMacro(macro, arg); + } + + /** Runs the specified macro or script file in the current thread. + The file is assumed to be in the macros folder + unless name is a full path. + The optional string argument (arg) can be retrieved in the called + macro or script using the getArgument() function. + Returns any string value returned by the macro, or null. Scripts always return null. + The equivalent macro function is runMacro(). */ + public static String runMacroFile(String name, String arg) { + Macro_Runner mr = new Macro_Runner(); + return mr.runMacroFile(name, arg); + } + + /** Runs the specified macro file. */ + public static String runMacroFile(String name) { + return runMacroFile(name, null); + } + + /** Runs the specified plugin using the specified image. */ + public static Object runPlugIn(ImagePlus imp, String className, String arg) { + if (imp!=null) { + ImagePlus temp = WindowManager.getTempCurrentImage(); + WindowManager.setTempCurrentImage(imp); + Object o = runPlugIn("", className, arg); + WindowManager.setTempCurrentImage(temp); + return o; + } else + return runPlugIn(className, arg); + } + + /** Runs the specified plugin and returns a reference to it. */ + public static Object runPlugIn(String className, String arg) { + return runPlugIn("", className, arg); + } + + /** Runs the specified plugin and returns a reference to it. */ + public static Object runPlugIn(String commandName, String className, String arg) { + if (arg==null) arg = ""; + if (IJ.debugMode) + IJ.log("runPlugIn: "+className+argument(arg)); + // Load using custom classloader if this is a user + // plugin and we are not running as an applet + if (!className.startsWith("ij.") && applet==null) + return runUserPlugIn(commandName, className, arg, false); + Object thePlugIn=null; + try { + Class c = Class.forName(className); + thePlugIn = c.newInstance(); + if (thePlugIn instanceof PlugIn) + ((PlugIn)thePlugIn).run(arg); + else + new PlugInFilterRunner(thePlugIn, commandName, arg); + } catch (ClassNotFoundException e) { + if (!(className!=null && className.startsWith("ij.plugin.MacAdapter"))) { + log("Plugin or class not found: \"" + className + "\"\n(" + e+")"); + String path = Prefs.getCustomPropsPath(); + if (path!=null); + log("Error may be due to custom properties at " + path); + } + } + catch (InstantiationException e) {log("Unable to load plugin (ins)");} + catch (IllegalAccessException e) {log("Unable to load plugin, possibly \nbecause it is not public.");} + redirectErrorMessages = false; + return thePlugIn; + } + + static Object runUserPlugIn(String commandName, String className, String arg, boolean createNewLoader) { + if (IJ.debugMode) + IJ.log("runUserPlugIn: "+className+", arg="+argument(arg)); + if (applet!=null) return null; + if (checkForDuplicatePlugins) { + // check for duplicate classes and jars in the plugins folder + IJ.runPlugIn("ij.plugin.ClassChecker", ""); + checkForDuplicatePlugins = false; + } + if (createNewLoader) + classLoader = null; + ClassLoader loader = getClassLoader(); + Object thePlugIn = null; + try { + thePlugIn = (loader.loadClass(className)).newInstance(); + if (thePlugIn instanceof PlugIn) + ((PlugIn)thePlugIn).run(arg); + else if (thePlugIn instanceof PlugInFilter) + new PlugInFilterRunner(thePlugIn, commandName, arg); + } + catch (ClassNotFoundException e) { + if (className.startsWith("macro:")) + runMacro(className.substring(6)); + else if (className.contains("_") && !suppressPluginNotFoundError) + error("Plugin or class not found: \"" + className + "\"\n(" + e+")"); + } + catch (NoClassDefFoundError e) { + int dotIndex = className.indexOf('.'); + if (dotIndex>=0 && className.contains("_")) { + // rerun plugin after removing folder name + if (debugMode) IJ.log("runUserPlugIn: rerunning "+className); + return runUserPlugIn(commandName, className.substring(dotIndex+1), arg, createNewLoader); + } + if (className.contains("_") && !suppressPluginNotFoundError) + error("Run User Plugin", "Class not found while attempting to run \"" + className + "\"\n \n " + e); + } + catch (InstantiationException e) {error("Unable to load plugin (ins)");} + catch (IllegalAccessException e) {error("Unable to load plugin, possibly \nbecause it is not public.");} + if (thePlugIn!=null && !"HandleExtraFileTypes".equals(className)) + redirectErrorMessages = false; + suppressPluginNotFoundError = false; + return thePlugIn; + } + + private static String argument(String arg) { + return arg!=null && !arg.equals("") && !arg.contains("\n")?"(\""+arg+"\")":""; + } + + static void wrongType(int capabilities, String cmd) { + String s = "\""+cmd+"\" requires an image of type:\n \n"; + if ((capabilities&PlugInFilter.DOES_8G)!=0) s += " 8-bit grayscale\n"; + if ((capabilities&PlugInFilter.DOES_8C)!=0) s += " 8-bit color\n"; + if ((capabilities&PlugInFilter.DOES_16)!=0) s += " 16-bit grayscale\n"; + if ((capabilities&PlugInFilter.DOES_32)!=0) s += " 32-bit (float) grayscale\n"; + if ((capabilities&PlugInFilter.DOES_RGB)!=0) s += " RGB color\n"; + error(s); + } + + /** Runs a menu command on a separete thread and returns immediately. */ + public static void doCommand(String command) { + new Executer(command, null); + } + + /** Runs a menu command on a separete thread, using the specified image. */ + public static void doCommand(ImagePlus imp, String command) { + new Executer(command, imp); + } + + /** Runs an ImageJ command. Does not return until + the command has finished executing. To avoid "image locked", + errors, plugins that call this method should implement + the PlugIn interface instead of PlugInFilter. */ + public static void run(String command) { + run(command, null); + } + + /** Runs an ImageJ command, with options that are passed to the + GenericDialog and OpenDialog classes. Does not return until + the command has finished executing. To generate run() calls, + start the recorder (Plugins/Macro/Record) and run commands + from the ImageJ menu bar. + */ + public static void run(String command, String options) { + //IJ.log("run1: "+command+" "+Thread.currentThread().hashCode()+" "+options); + if (ij==null && Menus.getCommands()==null) + init(); + Macro.abort = false; + Macro.setOptions(options); + Thread thread = Thread.currentThread(); + if (previousThread==null || thread!=previousThread) { + String name = thread.getName(); + if (!name.startsWith("Run$_")) + thread.setName("Run$_"+name); + } + command = convert(command); + previousThread = thread; + macroRunning = true; + Executer e = new Executer(command); + e.run(); + macroRunning = false; + Macro.setOptions(null); + testAbort(); + macroInterpreter = null; + //IJ.log("run2: "+command+" "+Thread.currentThread().hashCode()); + } + + /** The macro interpreter uses this method to run commands. */ + public static void run(Interpreter interpreter, String command, String options) { + macroInterpreter = interpreter; + run(command, options); + macroInterpreter = null; + } + + /** Converts commands that have been renamed so + macros using the old names continue to work. */ + private static String convert(String command) { + if (commandTable==null) { + commandTable = new Hashtable(30); + commandTable.put("New...", "Image..."); + commandTable.put("Threshold", "Make Binary"); + commandTable.put("Display...", "Appearance..."); + commandTable.put("Start Animation", "Start Animation [\\]"); + commandTable.put("Convert Images to Stack", "Images to Stack"); + commandTable.put("Convert Stack to Images", "Stack to Images"); + commandTable.put("Convert Stack to RGB", "Stack to RGB"); + commandTable.put("Convert to Composite", "Make Composite"); + commandTable.put("RGB Split", "Split Channels"); + commandTable.put("RGB Merge...", "Merge Channels..."); + commandTable.put("Channels...", "Channels Tool..."); + commandTable.put("New... ", "Table..."); + commandTable.put("Arbitrarily...", "Rotate... "); + commandTable.put("Measurements...", "Results... "); + commandTable.put("List Commands...", "Find Commands..."); + commandTable.put("Capture Screen ", "Capture Screen"); + commandTable.put("Add to Manager ", "Add to Manager"); + commandTable.put("In", "In [+]"); + commandTable.put("Out", "Out [-]"); + commandTable.put("Enhance Contrast", "Enhance Contrast..."); + commandTable.put("XY Coodinates... ", "XY Coordinates... "); + commandTable.put("Statistics...", "Statistics"); + commandTable.put("Channels Tool... ", "Channels Tool..."); + commandTable.put("Profile Plot Options...", "Plots..."); + commandTable.put("AuPbSn 40 (56K)", "AuPbSn 40"); + commandTable.put("Bat Cochlea Volume (19K)", "Bat Cochlea Volume"); + commandTable.put("Bat Cochlea Renderings (449K)", "Bat Cochlea Renderings"); + commandTable.put("Blobs (25K)", "Blobs"); + commandTable.put("Boats (356K)", "Boats"); + commandTable.put("Cardio (768K, RGB DICOM)", "Cardio (RGB DICOM)"); + commandTable.put("Cell Colony (31K)", "Cell Colony"); + commandTable.put("Clown (14K)", "Clown"); + commandTable.put("Confocal Series (2.2MB)", "Confocal Series"); + commandTable.put("CT (420K, 16-bit DICOM)", "CT (16-bit DICOM)"); + commandTable.put("Dot Blot (7K)", "Dot Blot"); + commandTable.put("Embryos (42K)", "Embryos"); + commandTable.put("Fluorescent Cells (400K)", "Fluorescent Cells"); + commandTable.put("Fly Brain (1MB)", "Fly Brain"); + commandTable.put("Gel (105K)", "Gel"); + commandTable.put("HeLa Cells (1.3M, 48-bit RGB)", "HeLa Cells (48-bit RGB)"); + commandTable.put("Leaf (36K)", "Leaf"); + commandTable.put("Line Graph (21K)", "Line Graph"); + commandTable.put("Mitosis (26MB, 5D stack)", "Mitosis (5D stack)"); + commandTable.put("MRI Stack (528K)", "MRI Stack"); + commandTable.put("M51 Galaxy (177K, 16-bits)", "M51 Galaxy (16-bits)"); + commandTable.put("Neuron (1.6M, 5 channels)", "Neuron (5 channels)"); + commandTable.put("Nile Bend (1.9M)", "Nile Bend"); + commandTable.put("Organ of Corti (2.8M, 4D stack)", "Organ of Corti (4D stack)"); + commandTable.put("Particles (75K)", "Particles"); + commandTable.put("T1 Head (2.4M, 16-bits)", "T1 Head (16-bits)"); + commandTable.put("T1 Head Renderings (736K)", "T1 Head Renderings"); + commandTable.put("TEM Filter (112K)", "TEM Filter"); + commandTable.put("Tree Rings (48K)", "Tree Rings"); + } + String command2 = (String)commandTable.get(command); + if (command2!=null) + return command2; + else + return command; + } + + /** Runs an ImageJ command using the specified image and options. + To generate run() calls, start the recorder (Plugins/Macro/Record) + and run commands from the ImageJ menu bar.*/ + public static void run(ImagePlus imp, String command, String options) { + if (ij==null && Menus.getCommands()==null) + init(); + if (imp!=null) { + ImagePlus temp = WindowManager.getTempCurrentImage(); + WindowManager.setTempCurrentImage(imp); + run(command, options); + WindowManager.setTempCurrentImage(temp); + } else + run(command, options); + } + + static void init() { + Menus m = new Menus(null, null); + Prefs.load(m, null); + m.addMenuBar(); + } + + private static void testAbort() { + if (Macro.abort) + abort(); + } + + /** Returns true if the run(), open() or newImage() method is executing. */ + public static boolean macroRunning() { + return macroRunning; + } + + /** Returns true if a macro is running, or if the run(), open() + or newImage() method is executing. */ + public static boolean isMacro() { + return macroRunning || Interpreter.getInstance()!=null; + } + + /**Returns the Applet that created this ImageJ or null if running as an application.*/ + public static java.applet.Applet getApplet() { + return applet; + } + + /**Displays a message in the ImageJ status bar. If 's' starts + with '!', subsequent showStatus() calls in the current + thread (without "!" in the message) are suppressed. */ + public static void showStatus(String s) { + if ((Interpreter.getInstance()==null&&statusBarThread==null) + || (statusBarThread!=null&&Thread.currentThread()!=statusBarThread)) + protectStatusBar(false); + boolean doProtect = s.startsWith("!"); // suppress subsequent showStatus() calls + if (doProtect) { + protectStatusBar(true); + statusBarThread = Thread.currentThread(); + s = s.substring(1); + } + if (doProtect || !protectStatusBar) { + if (ij!=null) + ij.showStatus(s); + ImagePlus imp = WindowManager.getCurrentImage(); + ImageCanvas ic = imp!=null?imp.getCanvas():null; + if (ic!=null) + ic.setShowCursorStatus(s.length()==0?true:false); + } + } + + /**Displays a message in the status bar and flashes + * either the status bar or the active image.
+ * See: http://wsr.imagej.net/macros/FlashingStatusMessages.txt + */ + public static void showStatus(String message, String options) { + showStatus(message); + if (options==null) + return; + options = options.replace("flash", ""); + options = options.replace("ms", ""); + Color optionalColor = null; + int index1 = options.indexOf("#"); + if (index1>=0) { // hex color? + int index2 = options.indexOf(" ", index1); + if (index2==-1) index2 = options.length(); + String hexColor = options.substring(index1, index2); + optionalColor = Colors.decode(hexColor, null); + options = options.replace(hexColor, ""); + } + if (optionalColor==null) { // "red", "green", etc. + for (String c : Colors.colors) { + if (options.contains(c)) { + optionalColor = Colors.getColor(c, ImageJ.backgroundColor); + options = options.replace(c, ""); + break; + } + } + } + boolean flashImage = options.contains("image"); + Color defaultColor = new Color(255,255,245); + int defaultDelay = 500; + ImagePlus imp = WindowManager.getCurrentImage(); + if (flashImage) { + options = options.replace("image", ""); + if (imp!=null && imp.getWindow()!=null) { + defaultColor = Color.black; + defaultDelay = 100; + } + else + flashImage = false; + } + Color color = optionalColor!=null?optionalColor:defaultColor; + int delay = (int)Tools.parseDouble(options, defaultDelay); + if (delay>8000) + delay = 8000; + String colorString = null; + ImageJ ij = IJ.getInstance(); + if (flashImage) { + Color previousColor = imp.getWindow().getBackground(); + imp.getWindow().setBackground(color); + if (delay>0) { + wait(delay); + imp.getWindow().setBackground(previousColor); + } + } else if (ij!=null) { + ij.getStatusBar().setBackground(color); + wait(delay); + ij.getStatusBar().setBackground(ij.backgroundColor); + } + } + + /** + * @deprecated + * replaced by IJ.log(), ResultsTable.setResult() and TextWindow.append(). + * There are examples at + * http://imagej.nih.gov/ij/plugins/sine-cosine.html + */ + public static void write(String s) { + if (textPanel==null && ij!=null) + showResults(); + if (textPanel!=null) + textPanel.append(s); + else + System.out.println(s); + } + + private static void showResults() { + TextWindow resultsWindow = new TextWindow("Results", "", 400, 250); + textPanel = resultsWindow.getTextPanel(); + textPanel.setResultsTable(Analyzer.getResultsTable()); + } + + public static synchronized void log(String s) { + if (s==null) return; + if (logPanel==null && ij!=null) { + TextWindow logWindow = new TextWindow("Log", "", 400, 250); + logPanel = logWindow.getTextPanel(); + logPanel.setFont(new Font("SansSerif", Font.PLAIN, 16)); + } + if (logPanel!=null) { + if (s.startsWith("\\")) + handleLogCommand(s); + else { + if (s.endsWith("\n")) { + if (s.equals("\n\n")) + s= "\n \n "; + else if (s.endsWith("\n\n")) + s = s.substring(0, s.length()-2)+"\n \n "; + else + s = s+" "; + } + logPanel.append(s); + } + } else { + LogStream.redirectSystem(false); + System.out.println(s); + } + } + + static void handleLogCommand(String s) { + if (s.equals("\\Closed")) + logPanel = null; + else if (s.startsWith("\\Update:")) { + int n = logPanel.getLineCount(); + String s2 = s.substring(8, s.length()); + if (n==0) + logPanel.append(s2); + else + logPanel.setLine(n-1, s2); + } else if (s.startsWith("\\Update")) { + int cindex = s.indexOf(":"); + if (cindex==-1) + {logPanel.append(s); return;} + String nstr = s.substring(7, cindex); + int line = (int)Tools.parseDouble(nstr, -1); + if (line<0 || line>25) + {logPanel.append(s); return;} + int count = logPanel.getLineCount(); + while (line>=count) { + log(""); + count++; + } + String s2 = s.substring(cindex+1, s.length()); + logPanel.setLine(line, s2); + } else if (s.equals("\\Clear")) { + logPanel.clear(); + } else if (s.startsWith("\\Heading:")) { + logPanel.updateColumnHeadings(s.substring(10)); + } else if (s.equals("\\Close")) { + Frame f = WindowManager.getFrame("Log"); + if (f!=null && (f instanceof TextWindow)) + ((TextWindow)f).close(); + } else + logPanel.append(s); + } + + /** Returns the contents of the Log window or null if the Log window is not open. */ + public static synchronized String getLog() { + if (logPanel==null || ij==null) + return null; + else + return logPanel.getText(); + } + +/** Clears the "Results" window and sets the column headings to + those in the tab-delimited 'headings' String. Writes to + System.out.println if the "ImageJ" frame is not present.*/ + public static void setColumnHeadings(String headings) { + if (textPanel==null && ij!=null) + showResults(); + if (textPanel!=null) + textPanel.setColumnHeadings(headings); + else + System.out.println(headings); + } + + /** Returns true if the "Results" window is open. */ + public static boolean isResultsWindow() { + return textPanel!=null; + } + + /** Renames a results window. */ + public static void renameResults(String title) { + Frame frame = WindowManager.getFrontWindow(); + if (frame!=null && (frame instanceof TextWindow)) { + TextWindow tw = (TextWindow)frame; + if (tw.getResultsTable()==null) { + IJ.error("Rename", "\""+tw.getTitle()+"\" is not a results table"); + return; + } + tw.rename(title); + } else if (isResultsWindow()) { + TextPanel tp = getTextPanel(); + TextWindow tw = (TextWindow)tp.getParent(); + tw.rename(title); + } + } + + /** Changes the name of a table window from 'oldTitle' to 'newTitle'. */ + public static void renameResults(String oldTitle, String newTitle) { + Frame frame = WindowManager.getFrame(oldTitle); + if (frame==null) { + error("Rename", "\""+oldTitle+"\" not found"); + return; + } else if (frame instanceof TextWindow) { + TextWindow tw = (TextWindow)frame; + if (tw.getResultsTable()==null) { + error("Rename", "\""+oldTitle+"\" is not a table"); + return; + } + tw.rename(newTitle); + } else + error("Rename", "\""+oldTitle+"\" is not a table"); + } + + /** Deletes 'row1' through 'row2' of the "Results" window, where + 'row1' and 'row2' must be in the range 0-Analyzer.getCounter()-1. */ + public static void deleteRows(int row1, int row2) { + ResultsTable rt = Analyzer.getResultsTable(); + int tableSize = rt.size(); + rt.deleteRows(row1, row2); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) + Overlay.updateTableOverlay(imp, row1, row2, tableSize); + rt.show("Results"); + } + + /** Returns a measurement result, where 'measurement' is "Area", + * "Mean", "StdDev", "Mode", "Min", "Max", "X", "Y", "XM", "YM", + * "Perim.", "BX", "BY", "Width", "Height", "Major", "Minor", "Angle", + * "Circ.", "Feret", "IntDen", "Median", "Skew", "Kurt", "%Area", + * "RawIntDen", "Ch", "Slice", "Frame", "FeretX", "FeretY", + * "FeretAngle", "MinFeret", "AR", "Round", "Solidity", "MinThr" + * or "MaxThr". Add " raw" to the argument to disable calibration, + * for example IJ.getValue("Mean raw"). Add " limit" to enable + * the "limit to threshold" option. + */ + public static double getValue(ImagePlus imp, String measurement) { + String options = ""; + int index = measurement.indexOf(" "); + if (index>0) { + if (index>>>>>>>>>>>>>>>>>>>>>>>>>>"); + log(""); + if (!memMessageDisplayed) { + log(""); + log(""); + log("Options>Memory & Threads command.>"); + log(">>>>>>>>>>>>>>>>>>>>>>>>>>>"); + memMessageDisplayed = true; + } + Macro.abort(); + } + + /** Updates the progress bar, where 0<=progress<=1.0. The progress bar is + not shown in BatchMode and erased if progress>=1.0. The progress bar is + updated only if more than 90 ms have passes since the last call. Does nothing + if the ImageJ window is not present. */ + public static void showProgress(double progress) { + if (progressBar!=null) progressBar.show(progress, false); + } + + /** Updates the progress bar, where the length of the bar is set to + * (currentValue+1)/finalValue of the maximum bar length. + * The bar is erased if currentValue>=finalValue. + * The bar is updated only if more than 90 ms have passed since the + * last call. Displays subordinate progress bars as dots if + * 'currentIndex' is negative (example: Plugins/Utilities/Benchmark). + */ + public static void showProgress(int currentIndex, int finalIndex) { + if (progressBar!=null) { + progressBar.show(currentIndex, finalIndex); + if (currentIndex==finalIndex) + progressBar.setBatchMode(false); + } + } + + /** Displays a message in a dialog box titled "Message". + Writes the Java console if ImageJ is not present. */ + public static void showMessage(String msg) { + showMessage("Message", msg); + } + + /** Displays a message in a dialog box with the specified title. + Displays HTML formatted text if 'msg' starts with "". + There are examples at + "http://imagej.nih.gov/ij/macros/HtmlDialogDemo.txt". + Writes to the Java console if ImageJ is not present. */ + public static void showMessage(String title, String msg) { + if (ij!=null) { + if (msg!=null && (msg.startsWith("")||msg.startsWith(""))) { + HTMLDialog hd = new HTMLDialog(title, msg); + if (isMacro() && hd.escapePressed()) + throw new RuntimeException(Macro.MACRO_CANCELED); + } else { + MessageDialog md = new MessageDialog(ij, title, msg); + if (isMacro() && md.escapePressed()) + throw new RuntimeException(Macro.MACRO_CANCELED); + } + } else + System.out.println(msg); + } + + /** Displays a message in a dialog box titled "ImageJ". If a + macro or JavaScript is running, it is aborted. Writes to the + Java console if the ImageJ window is not present.*/ + public static void error(String msg) { + error(null, msg); + if (Thread.currentThread().getName().endsWith("JavaScript")) + throw new RuntimeException(Macro.MACRO_CANCELED); + else + Macro.abort(); + } + + /** Displays a message in a dialog box with the specified title. If a + macro or JavaScript is running, it is aborted. Writes to the + Java console if the ImageJ window is not present. */ + public static void error(String title, String msg) { + if (macroInterpreter!=null) { + macroInterpreter.abort(msg); + macroInterpreter = null; + return; + } + if (msg!=null && msg.endsWith(Macro.MACRO_CANCELED)) + return; + String title2 = title!=null?title:"ImageJ"; + boolean abortMacro = title!=null; + lastErrorMessage = msg; + if (redirectErrorMessages) { + IJ.log(title2 + ": " + msg); + if (abortMacro && (title.contains("Open")||title.contains("Reader"))) + abortMacro = false; + } else + showMessage(title2, msg); + redirectErrorMessages = false; + if (abortMacro) + Macro.abort(); + } + + /** Aborts any currently running JavaScript, or use IJ.error(string) + to abort a JavaScript with a message. */ + public static void exit() { + if (Thread.currentThread().getName().endsWith("JavaScript")) + throw new RuntimeException(Macro.MACRO_CANCELED); + } + + /** + * Returns the last error message written by IJ.error() or null if there + * was no error since the last time this method was called. + * @see #error(String) + */ + public static String getErrorMessage() { + String msg = lastErrorMessage; + lastErrorMessage = null; + return msg; + } + + /** Displays a message in a dialog box with the specified title. + Returns false if the user pressed "Cancel". */ + public static boolean showMessageWithCancel(String title, String msg) { + GenericDialog gd = new GenericDialog(title); + gd.addMessage(msg); + gd.showDialog(); + return !gd.wasCanceled(); + } + + public static final int CANCELED = Integer.MIN_VALUE; + + /** Allows the user to enter a number in a dialog box. Returns the + value IJ.CANCELED (-2,147,483,648) if the user cancels the dialog box. + Returns 'defaultValue' if the user enters an invalid number. */ + public static double getNumber(String prompt, double defaultValue) { + GenericDialog gd = new GenericDialog(""); + int decimalPlaces = (int)defaultValue==defaultValue?0:2; + gd.addNumericField(prompt, defaultValue, decimalPlaces); + gd.showDialog(); + if (gd.wasCanceled()) + return CANCELED; + double v = gd.getNextNumber(); + if (gd.invalidNumber()) + return defaultValue; + else + return v; + } + + /** Allows the user to enter a string in a dialog box. Returns + "" if the user cancels the dialog box. */ + public static String getString(String prompt, String defaultString) { + GenericDialog gd = new GenericDialog(""); + gd.addStringField(prompt, defaultString, 20); + gd.showDialog(); + if (gd.wasCanceled()) + return ""; + return gd.getNextString(); + } + + /**Delays 'msecs' milliseconds.*/ + public static void wait(int msecs) { + try {Thread.sleep(msecs);} + catch (InterruptedException e) { } + } + + /** Emits an audio beep. */ + public static void beep() { + java.awt.Toolkit.getDefaultToolkit().beep(); + } + + /** Returns a string something like "64K of 256MB (25%)" + * that shows how much of the available memory is in use. + * This is the string displayed when the user clicks in the + * status bar. + */ + public static String freeMemory() { + long inUse = currentMemory(); + String inUseStr = inUse<10000*1024?inUse/1024L+"K":inUse/1048576L+"MB"; + String maxStr=""; + long max = maxMemory(); + if (max>0L) { + double percent = inUse*100/max; + maxStr = " of "+max/1048576L+"MB ("+(percent<1.0?"<1":d2s(percent,0)) + "%)"; + } + return inUseStr + maxStr; + } + + /** Returns the amount of memory currently being used by ImageJ. */ + public static long currentMemory() { + long freeMem = Runtime.getRuntime().freeMemory(); + long totMem = Runtime.getRuntime().totalMemory(); + return totMem-freeMem; + } + + /** Returns the maximum amount of memory available to ImageJ or + zero if ImageJ is unable to determine this limit. */ + public static long maxMemory() { + if (maxMemory==0L) { + Memory mem = new Memory(); + maxMemory = mem.getMemorySetting(); + if (maxMemory==0L) maxMemory = mem.maxMemory(); + } + return maxMemory; + } + + public static void showTime(ImagePlus imp, long start, String str) { + showTime(imp, start, str, 1); + } + + public static void showTime(ImagePlus imp, long start, String str, int nslices) { + if (Interpreter.isBatchMode()) + return; + double seconds = (System.currentTimeMillis()-start)/1000.0; + if (seconds<=0.5 && macroRunning()) + return; + double pixels = (double)imp.getWidth() * imp.getHeight(); + double rate = pixels*nslices/seconds; + String str2; + if (rate>1000000000.0) + str2 = ""; + else if (rate<1000000.0) + str2 = ", "+d2s(rate,0)+" pixels/second"; + else + str2 = ", "+d2s(rate/1000000.0,1)+" million pixels/second"; + showStatus(str+seconds+" seconds"+str2); + } + + /** Experimental */ + public static String time(ImagePlus imp, long startNanoTime) { + double planes = imp.getStackSize(); + double seconds = (System.nanoTime()-startNanoTime)/1000000000.0; + double mpixels = imp.getWidth()*imp.getHeight()*planes/1000000.0; + String time = seconds<1.0?d2s(seconds*1000.0,0)+" ms":d2s(seconds,1)+" seconds"; + return time+", "+d2s(mpixels/seconds,1)+" million pixels/second"; + } + + /** Converts a number to a formatted string using + 2 digits to the right of the decimal point. */ + public static String d2s(double n) { + return d2s(n, 2); + } + + /** Converts a number to a rounded formatted string. + The 'decimalPlaces' argument specifies the number of + digits to the right of the decimal point (0-9). Uses + scientific notation if 'decimalPlaces is negative. */ + public static String d2s(double n, int decimalPlaces) { + if (Double.isNaN(n)||Double.isInfinite(n)) + return ""+n; + if (n==Float.MAX_VALUE) // divide by 0 in FloatProcessor + return "3.4e38"; + double np = n; + if (n<0.0) np = -n; + if (decimalPlaces<0) synchronized(IJ.class) { + decimalPlaces = -decimalPlaces; + if (decimalPlaces>9) decimalPlaces=9; + if (sf==null) { + if (dfs==null) + dfs = new DecimalFormatSymbols(Locale.US); + sf = new DecimalFormat[10]; + sf[1] = new DecimalFormat("0.0E0",dfs); + sf[2] = new DecimalFormat("0.00E0",dfs); + sf[3] = new DecimalFormat("0.000E0",dfs); + sf[4] = new DecimalFormat("0.0000E0",dfs); + sf[5] = new DecimalFormat("0.00000E0",dfs); + sf[6] = new DecimalFormat("0.000000E0",dfs); + sf[7] = new DecimalFormat("0.0000000E0",dfs); + sf[8] = new DecimalFormat("0.00000000E0",dfs); + sf[9] = new DecimalFormat("0.000000000E0",dfs); + } + return sf[decimalPlaces].format(n); // use scientific notation + } + if (decimalPlaces<0) decimalPlaces = 0; + if (decimalPlaces>9) decimalPlaces = 9; + return df[decimalPlaces].format(n); + } + + /** Converts a number to a rounded formatted string. + * The 'significantDigits' argument specifies the minimum number + * of significant digits, which is also the preferred number of + * digits behind the decimal. Fewer decimals are shown if the + * number would have more than 'maxDigits'. + * Exponential notation is used if more than 'maxDigits' would be needed. + */ + public static String d2s(double x, int significantDigits, int maxDigits) { + double log10 = Math.log10(Math.abs(x)); + double roundErrorAtMax = 0.223*Math.pow(10, -maxDigits); + int magnitude = (int)Math.ceil(log10+roundErrorAtMax); + int decimals = x==0 ? 0 : maxDigits - magnitude; + if (decimals<0 || magnitudesignificantDigits) + decimals = Math.max(significantDigits, decimals-maxDigits+significantDigits); + return IJ.d2s(x, decimals); + } + } + + /** Pad 'n' with leading zeros to the specified number of digits. */ + public static String pad(int n, int digits) { + String str = ""+n; + while (str.length()= 6; + } + + /** Returns true if ImageJ is running on a Java 1.7 or greater JVM. */ + public static boolean isJava17() { + return javaVersion >= 7; + } + + /** Returns true if ImageJ is running on a Java 1.8 or greater JVM. */ + public static boolean isJava18() { + return javaVersion >= 8; + } + + /** Returns true if ImageJ is running on a Java 1.9 or greater JVM. */ + public static boolean isJava19() { + return javaVersion >= 9; + } + + /** Returns true if ImageJ is running on Linux. */ + public static boolean isLinux() { + return isLinux; + } + + /** Obsolete; always returns false. */ + public static boolean isVista() { + return false; + } + + /** Returns true if ImageJ is running a 64-bit version of Java. */ + public static boolean is64Bit() { + if (osarch==null) + osarch = System.getProperty("os.arch"); + return osarch!=null && osarch.indexOf("64")!=-1; + } + + /** Displays an error message and returns true if the + ImageJ version is less than the one specified. */ + public static boolean versionLessThan(String version) { + boolean lessThan = ImageJ.VERSION.compareTo(version)<0; + if (lessThan) + error("This plugin or macro requires ImageJ "+version+" or later. Use\nHelp>Update ImageJ to upgrade to the latest version."); + return lessThan; + } + + /** Displays a "Process all images?" dialog. Returns + 'flags'+PlugInFilter.DOES_STACKS if the user selects "Yes", + 'flags' if the user selects "No" and PlugInFilter.DONE + if the user selects "Cancel". + */ + public static int setupDialog(ImagePlus imp, int flags) { + if (imp==null || (ij!=null&&ij.hotkey)) { + if (ij!=null) ij.hotkey=false; + return flags; + } + int stackSize = imp.getStackSize(); + if (stackSize>1) { + String macroOptions = Macro.getOptions(); + if (imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.COMPOSITE) { + if (macroOptions==null || !macroOptions.contains("slice")) + return flags | PlugInFilter.DOES_STACKS; + } + if (macroOptions!=null) { + if (macroOptions.indexOf("stack ")>=0) + return flags | PlugInFilter.DOES_STACKS; + else + return flags; + } + if (hideProcessStackDialog) + return flags; + String note = ((flags&PlugInFilter.NO_CHANGES)==0)?" There is\nno Undo if you select \"Yes\".":""; + YesNoCancelDialog d = new YesNoCancelDialog(getInstance(), + "Process Stack?", "Process all "+stackSize+" images?"+note); + if (d.cancelPressed()) + return PlugInFilter.DONE; + else if (d.yesPressed()) { + if (imp.getStack().isVirtual() && ((flags&PlugInFilter.NO_CHANGES)==0)) { + int size = (stackSize*imp.getWidth()*imp.getHeight()*imp.getBytesPerPixel()+524288)/1048576; + String msg = + "Use the Process>Batch>Virtual Stack command\n"+ + "to process a virtual stack or convert it into a\n"+ + "normal stack using Image>Duplicate, which\n"+ + "will require "+size+"MB of additional memory."; + error(msg); + return PlugInFilter.DONE; + } + if (Recorder.record) + Recorder.recordOption("stack"); + return flags | PlugInFilter.DOES_STACKS; + } + if (Recorder.record) + Recorder.recordOption("slice"); + } + return flags; + } + + /** Creates a rectangular selection. Removes any existing + selection if width or height are less than 1. */ + public static void makeRectangle(int x, int y, int width, int height) { + if (width<=0 || height<0) + getImage().deleteRoi(); + else { + ImagePlus img = getImage(); + if (Interpreter.isBatchMode()) + img.setRoi(new Roi(x,y,width,height), false); + else + img.setRoi(x, y, width, height); + } + } + + /** Creates a subpixel resolution rectangular selection. */ + public static void makeRectangle(double x, double y, double width, double height) { + if (width<=0 || height<0) + getImage().deleteRoi(); + else + getImage().setRoi(new Roi(x,y,width,height), !Interpreter.isBatchMode()); + } + + /** Creates an oval selection. Removes any existing + selection if width or height are less than 1. */ + public static void makeOval(int x, int y, int width, int height) { + if (width<=0 || height<0) + getImage().deleteRoi(); + else { + ImagePlus img = getImage(); + img.setRoi(new OvalRoi(x, y, width, height)); + } + } + + /** Creates an subpixel resolution oval selection. */ + public static void makeOval(double x, double y, double width, double height) { + if (width<=0 || height<0) + getImage().deleteRoi(); + else + getImage().setRoi(new OvalRoi(x, y, width, height)); + } + + /** Creates a straight line selection. */ + public static void makeLine(int x1, int y1, int x2, int y2) { + getImage().setRoi(new Line(x1, y1, x2, y2)); + } + + /** Creates a straight line selection using floating point coordinates. */ + public static void makeLine(double x1, double y1, double x2, double y2) { + getImage().setRoi(new Line(x1, y1, x2, y2)); + } + + /** Creates a point selection using integer coordinates.. */ + public static void makePoint(int x, int y) { + ImagePlus img = getImage(); + Roi roi = img.getRoi(); + if (shiftKeyDown() && roi!=null && roi.getType()==Roi.POINT) { + Polygon p = roi.getPolygon(); + p.addPoint(x, y); + img.setRoi(new PointRoi(p.xpoints, p.ypoints, p.npoints)); + IJ.setKeyUp(KeyEvent.VK_SHIFT); + } else if (altKeyDown() && roi!=null && roi.getType()==Roi.POINT) { + ((PolygonRoi)roi).deleteHandle(x, y); + IJ.setKeyUp(KeyEvent.VK_ALT); + } else + img.setRoi(new PointRoi(x, y)); + } + + /** Creates a point selection using floating point coordinates. */ + public static void makePoint(double x, double y) { + ImagePlus img = getImage(); + Roi roi = img.getRoi(); + if (shiftKeyDown() && roi!=null && roi.getType()==Roi.POINT) { + Polygon p = roi.getPolygon(); + p.addPoint((int)Math.round(x), (int)Math.round(y)); + img.setRoi(new PointRoi(p.xpoints, p.ypoints, p.npoints)); + IJ.setKeyUp(KeyEvent.VK_SHIFT); + } else if (altKeyDown() && roi!=null && roi.getType()==Roi.POINT) { + ((PolygonRoi)roi).deleteHandle(x, y); + IJ.setKeyUp(KeyEvent.VK_ALT); + } else + img.setRoi(new PointRoi(x, y)); + } + + /** Creates an Roi. */ + public static Roi Roi(double x, double y, double width, double height) { + return new Roi(x, y, width, height); + } + + /** Creates an OvalRoi. */ + public static OvalRoi OvalRoi(double x, double y, double width, double height) { + return new OvalRoi(x, y, width, height); + } + + /** Sets the display range (minimum and maximum displayed pixel values) of the current image. */ + public static void setMinAndMax(double min, double max) { + setMinAndMax(getImage(), min, max, 7); + } + + /** Sets the display range (minimum and maximum displayed pixel values) of the specified image. */ + public static void setMinAndMax(ImagePlus img, double min, double max) { + setMinAndMax(img, min, max, 7); + } + + /** Sets the minimum and maximum displayed pixel values on the specified RGB + channels, where 4=red, 2=green and 1=blue. */ + public static void setMinAndMax(double min, double max, int channels) { + setMinAndMax(getImage(), min, max, channels); + } + + private static void setMinAndMax(ImagePlus img, double min, double max, int channels) { + Calibration cal = img.getCalibration(); + min = cal.getRawValue(min); + max = cal.getRawValue(max); + if (channels==7) + img.setDisplayRange(min, max); + else + img.setDisplayRange(min, max, channels); + img.updateAndDraw(); + } + + /** Resets the minimum and maximum displayed pixel values of the + current image to be the same as the min and max pixel values. */ + public static void resetMinAndMax() { + resetMinAndMax(getImage()); + } + + /** Resets the minimum and maximum displayed pixel values of the + specified image to be the same as the min and max pixel values. */ + public static void resetMinAndMax(ImagePlus img) { + img.resetDisplayRange(); + img.updateAndDraw(); + } + + /** Sets the lower and upper threshold levels and displays the image + using red to highlight thresholded pixels. May not work correctly on + 16 and 32 bit images unless the display range has been reset using IJ.resetMinAndMax(). + */ + public static void setThreshold(double lowerThreshold, double upperThresold) { + setThreshold(lowerThreshold, upperThresold, null); + } + + /** Sets the lower and upper threshold levels and displays the image using + the specified displayMode ("Red", "Black & White", "Over/Under" or "No Update"). */ + public static void setThreshold(double lowerThreshold, double upperThreshold, String displayMode) { + setThreshold(getImage(), lowerThreshold, upperThreshold, displayMode); + } + + /** Sets the lower and upper threshold levels of the specified image. */ + public static void setThreshold(ImagePlus img, double lowerThreshold, double upperThreshold) { + setThreshold(img, lowerThreshold, upperThreshold, "Red"); + } + + /** Sets the lower and upper threshold levels of the specified image and updates the display using + the specified displayMode ("Red", "Black & White", "Over/Under" or "No Update"). + With calibrated images, 'lowerThreshold' and 'upperThreshold' must be density calibrated values. + Use setRawThreshold() to set the threshold using raw (uncalibrated) values. */ + public static void setThreshold(ImagePlus img, double lowerThreshold, double upperThreshold, String displayMode) { + Calibration cal = img.getCalibration(); + if (displayMode==null || !displayMode.contains("raw")) { + lowerThreshold = cal.getRawValue(lowerThreshold); + upperThreshold = cal.getRawValue(upperThreshold); + } + setRawThreshold(img, lowerThreshold, upperThreshold, displayMode); + } + + /** This is a version of setThreshold() that always uses raw (uncalibrated) values + in the range 0-255 for 8-bit images and 0-65535 for 16-bit images. */ + public static void setRawThreshold(ImagePlus img, double lowerThreshold, double upperThreshold, String displayMode) { + int mode = ImageProcessor.RED_LUT; + if (displayMode!=null) { + displayMode = displayMode.toLowerCase(Locale.US); + if (displayMode.contains("black")) + mode = ImageProcessor.BLACK_AND_WHITE_LUT; + else if (displayMode.contains("over")) + mode = ImageProcessor.OVER_UNDER_LUT; + else if (displayMode.contains("no")) + mode = ImageProcessor.NO_LUT_UPDATE; + } + img.getProcessor().setThreshold(lowerThreshold, upperThreshold, mode); + if (mode!=ImageProcessor.NO_LUT_UPDATE && img.getWindow()!=null) { + img.getProcessor().setLutAnimation(true); + img.updateAndDraw(); + ThresholdAdjuster.update(); + } + } + + public static void setAutoThreshold(ImagePlus imp, String method) { + ImageProcessor ip = imp.getProcessor(); + if (ip instanceof ColorProcessor) + throw new IllegalArgumentException("Non-RGB image required"); + ip.setRoi(imp.getRoi()); + if (method!=null) { + try { + if (method.indexOf("stack")!=-1) + setStackThreshold(imp, ip, method); + else + ip.setAutoThreshold(method); + } catch (Exception e) { + log(e.getMessage()); + } + } else + ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.RED_LUT); + imp.updateAndDraw(); + } + + private static void setStackThreshold(ImagePlus imp, ImageProcessor ip, String method) { + boolean darkBackground = method.indexOf("dark")!=-1; + int measurements = Analyzer.getMeasurements(); + Analyzer.setMeasurements(Measurements.AREA+Measurements.MIN_MAX); + ImageStatistics stats = new StackStatistics(imp); + Analyzer.setMeasurements(measurements); + AutoThresholder thresholder = new AutoThresholder(); + double min=0.0, max=255.0; + if (imp.getBitDepth()!=8) { + min = stats.min; + max = stats.max; + } + int threshold = thresholder.getThreshold(method, stats.histogram); + double lower, upper; + if (darkBackground) { + if (ip.isInvertedLut()) + {lower=0.0; upper=threshold;} + else + {lower=threshold+1; upper=255.0;} + } else { + if (ip.isInvertedLut()) + {lower=threshold+1; upper=255.0;} + else + {lower=0.0; upper=threshold;} + } + if (lower>255) lower = 255; + if (max>min) { + lower = min + (lower/255.0)*(max-min); + upper = min + (upper/255.0)*(max-min); + } else + lower = upper = min; + ip.setMinAndMax(min, max); + ip.setThreshold(lower, upper, ImageProcessor.RED_LUT); + imp.updateAndDraw(); + } + + /** Disables thresholding on the current image. */ + public static void resetThreshold() { + resetThreshold(getImage()); + } + + /** Disables thresholding on the specified image. */ + public static void resetThreshold(ImagePlus img) { + ImageProcessor ip = img.getProcessor(); + ip.resetThreshold(); + ip.setLutAnimation(true); + img.updateAndDraw(); + ThresholdAdjuster.update(); + } + + /** For IDs less than zero, activates the image with the specified ID. + For IDs greater than zero, activates the Nth image. */ + public static void selectWindow(int id) { + if (id>0) + id = WindowManager.getNthImageID(id); + ImagePlus imp = WindowManager.getImage(id); + if (imp==null) + error("Macro Error", "Image "+id+" not found or no images are open."); + if (Interpreter.isBatchMode()) { + ImagePlus impT = WindowManager.getTempCurrentImage(); + ImagePlus impC = WindowManager.getCurrentImage(); + if (impC!=null && impC!=imp && impT!=null) + impC.saveRoi(); + WindowManager.setTempCurrentImage(imp); + Interpreter.activateImage(imp); + WindowManager.setWindow(null); + } else { + if (imp==null) + return; + ImageWindow win = imp.getWindow(); + if (win!=null) { + win.toFront(); + win.setState(Frame.NORMAL); + WindowManager.setWindow(win); + } + long start = System.currentTimeMillis(); + // timeout after 1 second unless current thread is event dispatch thread + String thread = Thread.currentThread().getName(); + int timeout = thread!=null&&thread.indexOf("EventQueue")!=-1?0:1000; + if (IJ.isMacOSX() && IJ.isJava18() && timeout>0) + timeout = 250; //work around OS X/Java 8 window activation bug + while (true) { + wait(10); + imp = WindowManager.getCurrentImage(); + if (imp!=null && imp.getID()==id) + return; // specified image is now active + if ((System.currentTimeMillis()-start)>timeout && win!=null) { + WindowManager.setCurrentWindow(win); + return; + } + } + } + } + + /** Activates the window with the specified title. */ + public static void selectWindow(String title) { + if (title.equals("ImageJ")&&ij!=null) { + ij.toFront(); + return; + } + long start = System.currentTimeMillis(); + while (System.currentTimeMillis()-start<3000) { // 3 sec timeout + Window win = WindowManager.getWindow(title); + if (win!=null && !(win instanceof ImageWindow)) { + selectWindow(win); + return; + } + int[] wList = WindowManager.getIDList(); + int len = wList!=null?wList.length:0; + for (int i=0; i1000) { + WindowManager.setWindow(win); + return; // 1 second timeout + } + } + } + + /** Sets the foreground color. */ + public static void setForegroundColor(int red, int green, int blue) { + setColor(red, green, blue, true); + } + + /** Sets the background color. */ + public static void setBackgroundColor(int red, int green, int blue) { + setColor(red, green, blue, false); + } + + static void setColor(int red, int green, int blue, boolean foreground) { + Color c = Colors.toColor(red, green, blue); + if (foreground) { + Toolbar.setForegroundColor(c); + ImagePlus img = WindowManager.getCurrentImage(); + if (img!=null) + img.getProcessor().setColor(c); + } else + Toolbar.setBackgroundColor(c); + } + + /** Switches to the specified tool, where id = Toolbar.RECTANGLE (0), + Toolbar.OVAL (1), etc. */ + public static void setTool(int id) { + Toolbar.getInstance().setTool(id); + } + + /** Switches to the specified tool, where 'name' is "rect", "elliptical", + "brush", etc. Returns 'false' if the name is not recognized. */ + public static boolean setTool(String name) { + return Toolbar.getInstance().setTool(name); + } + + /** Returns the name of the current tool. */ + public static String getToolName() { + return Toolbar.getToolName(); + } + + /** Equivalent to clicking on the current image at (x,y) with the + wand tool. Returns the number of points in the resulting ROI. */ + public static int doWand(int x, int y) { + return doWand(getImage(), x, y, 0, null); + } + + /** Traces the boundary of the area with pixel values within + * 'tolerance' of the value of the pixel at the starting location. + * 'tolerance' is in uncalibrated units. + * 'mode' can be "4-connected", "8-connected" or "Legacy". + * "Legacy" is for compatibility with previous versions of ImageJ; + * it is ignored if 'tolerance' > 0. + */ + public static int doWand(int x, int y, double tolerance, String mode) { + return doWand(getImage(), x, y, tolerance, mode); + } + + /** This version of doWand adds an ImagePlus argument. */ + public static int doWand(ImagePlus img, int x, int y, double tolerance, String mode) { + ImageProcessor ip = img.getProcessor(); + if ((img.getType()==ImagePlus.GRAY32) && Double.isNaN(ip.getPixelValue(x,y))) + return 0; + int imode = Wand.LEGACY_MODE; + boolean smooth = false; + if (mode!=null) { + if (mode.startsWith("4")) + imode = Wand.FOUR_CONNECTED; + else if (mode.startsWith("8")) + imode = Wand.EIGHT_CONNECTED; + smooth = mode.contains("smooth"); + + } + Wand w = new Wand(ip); + double t1 = ip.getMinThreshold(); + if (t1==ImageProcessor.NO_THRESHOLD || (ip.getLutUpdateMode()==ImageProcessor.NO_LUT_UPDATE&& tolerance>0.0)) { + w.autoOutline(x, y, tolerance, imode); + smooth = false; + } else + w.autoOutline(x, y, t1, ip.getMaxThreshold(), imode); + if (w.npoints>0) { + Roi previousRoi = img.getRoi(); + Roi roi = new PolygonRoi(w.xpoints, w.ypoints, w.npoints, Roi.TRACED_ROI); + img.deleteRoi(); + img.setRoi(roi); + if (previousRoi!=null) + roi.update(shiftKeyDown(), altKeyDown()); // add/subtract ROI to previous one if shift/alt key down + Roi roi2 = img.getRoi(); + if (smooth && roi2!=null && roi2.getType()==Roi.TRACED_ROI) { + Rectangle bounds = roi2.getBounds(); + if (bounds.width>1 && bounds.height>1) { + if (smoothMacro==null) + smoothMacro = BatchProcessor.openMacroFromJar("SmoothWandTool.txt"); + if (EventQueue.isDispatchThread()) + new MacroRunner(smoothMacro); // run on separate thread + else + Macro.eval(smoothMacro); + } + } + } + return w.npoints; + } + + /** Sets the transfer mode used by the Edit/Paste command, where mode is "Copy", "Blend", "Average", "Difference", + "Transparent", "Transparent2", "AND", "OR", "XOR", "Add", "Subtract", "Multiply", or "Divide". */ + public static void setPasteMode(String mode) { + Roi.setPasteMode(stringToPasteMode(mode)); + } + + public static int stringToPasteMode(String mode) { + if (mode==null) + return Blitter.COPY; + mode = mode.toLowerCase(Locale.US); + int m = Blitter.COPY; + if (mode.startsWith("ble") || mode.startsWith("ave")) + m = Blitter.AVERAGE; + else if (mode.startsWith("diff")) + m = Blitter.DIFFERENCE; + else if (mode.indexOf("zero")!=-1) + m = Blitter.COPY_ZERO_TRANSPARENT; + else if (mode.startsWith("tran")) + m = Blitter.COPY_TRANSPARENT; + else if (mode.startsWith("and")) + m = Blitter.AND; + else if (mode.startsWith("or")) + m = Blitter.OR; + else if (mode.startsWith("xor")) + m = Blitter.XOR; + else if (mode.startsWith("sub")) + m = Blitter.SUBTRACT; + else if (mode.startsWith("add")) + m = Blitter.ADD; + else if (mode.startsWith("div")) + m = Blitter.DIVIDE; + else if (mode.startsWith("mul")) + m = Blitter.MULTIPLY; + else if (mode.startsWith("min")) + m = Blitter.MIN; + else if (mode.startsWith("max")) + m = Blitter.MAX; + return m; + } + + /** Returns a reference to the active image, or displays an error + message and aborts the plugin or macro if no images are open. */ + public static ImagePlus getImage() { + ImagePlus img = WindowManager.getCurrentImage(); + if (img==null) { + IJ.noImage(); + if (ij==null) + System.exit(0); + else + abort(); + } + return img; + } + + /**The macro interpreter uses this method to call getImage().*/ + public static ImagePlus getImage(Interpreter interpreter) { + macroInterpreter = interpreter; + ImagePlus imp = getImage(); + macroInterpreter = null; + return imp; + } + + /** Returns the active image or stack slice as an ImageProcessor, or displays + an error message and aborts the plugin or macro if no images are open. */ + public static ImageProcessor getProcessor() { + ImagePlus imp = IJ.getImage(); + return imp.getProcessor(); + } + + /** Switches to the specified stack slice, where 1<='slice'<=stack-size. */ + public static void setSlice(int slice) { + getImage().setSlice(slice); + } + + /** Returns the ImageJ version number as a string. */ + public static String getVersion() { + return ImageJ.VERSION; + } + + /** Returns the ImageJ version and build number as a String, for + example "1.46n05", or 1.46n99 if there is no build number. */ + public static String getFullVersion() { + String build = ImageJ.BUILD; + if (build.length()==0) + build = "99"; + else if (build.length()==1) + build = "0" + build; + return ImageJ.VERSION+build; + } + + /** Returns the path to the specified directory if title is + "home" ("user.home"), "downloads", "startup", "imagej" (ImageJ directory), + "plugins", "macros", "luts", "temp", "current", "default", + "image" (directory active image was loaded from), "file" + (directory most recently used to open or save a file) or "cwd" + (current working directory), otherwise displays a dialog and + returns the path to the directory selected by the user. Returns + null if the specified directory is not found or the user cancels the + dialog box. Also aborts the macro if the user cancels the + dialog box.*/ + public static String getDirectory(String title) { + String dir = null; + String title2 = title.toLowerCase(Locale.US); + if (title2.equals("plugins")) + dir = Menus.getPlugInsPath(); + else if (title2.equals("macros")) + dir = Menus.getMacrosPath(); + else if (title2.equals("luts")) { + String ijdir = Prefs.getImageJDir(); + if (ijdir!=null) + dir = ijdir + "luts" + File.separator; + else + dir = null; + } else if (title2.equals("home")) + dir = System.getProperty("user.home"); + else if (title2.equals("downloads")) + dir = System.getProperty("user.home")+File.separator+"Downloads"; + else if (title2.equals("startup")) + dir = Prefs.getImageJDir(); + else if (title2.equals("imagej")) + dir = Prefs.getImageJDir(); + else if (title2.equals("current") || title2.equals("default")) + dir = OpenDialog.getDefaultDirectory(); + else if (title2.equals("temp")) { + dir = System.getProperty("java.io.tmpdir"); + if (isMacintosh()) dir = "/tmp/"; + } else if (title2.equals("image")) { + ImagePlus imp = WindowManager.getCurrentImage(); + FileInfo fi = imp!=null?imp.getOriginalFileInfo():null; + if (fi!=null && fi.directory!=null) { + dir = fi.directory; + } else + dir = null; + } else if (title2.equals("file")) + dir = OpenDialog.getLastDirectory(); + else if (title2.equals("cwd")) + dir = System.getProperty("user.dir"); + else { + DirectoryChooser dc = new DirectoryChooser(title); + dir = dc.getDirectory(); + if (dir==null) Macro.abort(); + } + dir = addSeparator(dir); + return dir; + } + + public static String addSeparator(String path) { + if (path==null) + return null; + if (path.length()>0 && !(path.endsWith(File.separator)||path.endsWith("/"))) { + if (IJ.isWindows()&&path.contains(File.separator)) + path += File.separator; + else + path += "/"; + } + return path; + } + + /** Alias for getDirectory(). */ + public static String getDir(String title) { + return getDirectory(title); + } + + + /** Displays an open file dialog and returns the path to the + choosen file, or returns null if the dialog is canceled. */ + public static String getFilePath(String dialogTitle) { + OpenDialog od = new OpenDialog(dialogTitle); + return od.getPath(); + } + + /** Displays a file open dialog box and then opens the tiff, dicom, + fits, pgm, jpeg, bmp, gif, lut, roi, or text file selected by + the user. Displays an error message if the selected file is not + in one of the supported formats, or if it is not found. */ + public static void open() { + open(null); + } + + /** Opens and displays a tiff, dicom, fits, pgm, jpeg, bmp, gif, lut, + roi, or text file. Displays an error message if the specified file + is not in one of the supported formats, or if it is not found. + With 1.41k or later, opens images specified by a URL. + */ + public static void open(String path) { + if (ij==null && Menus.getCommands()==null) + init(); + Opener o = new Opener(); + macroRunning = true; + if (path==null || path.equals("")) + o.open(); + else + o.open(path); + macroRunning = false; + } + + /** Opens and displays the nth image in the specified tiff stack. */ + public static void open(String path, int n) { + if (ij==null && Menus.getCommands()==null) + init(); + ImagePlus imp = openImage(path, n); + if (imp!=null) imp.show(); + } + + /** Opens the specified file as a tiff, bmp, dicom, fits, pgm, gif, jpeg + or text image and returns an ImagePlus object if successful. + Calls HandleExtraFileTypes plugin if the file type is not recognised. + Displays a file open dialog if 'path' is null or an empty string. + Note that 'path' can also be a URL. Some reader plugins, including + the Bio-Formats plugin, display the image and return null. + Use IJ.open() to display a file open dialog box. + */ + public static ImagePlus openImage(String path) { + macroRunning = true; + ImagePlus imp = (new Opener()).openImage(path); + macroRunning = false; + return imp; + } + + /** Opens the nth image of the specified tiff stack. */ + public static ImagePlus openImage(String path, int n) { + return (new Opener()).openImage(path, n); + } + + /** Opens the specified tiff file as a virtual stack. */ + public static ImagePlus openVirtual(String path) { + return FileInfoVirtualStack.openVirtual(path); + } + + /** Opens an image using a file open dialog and returns it as an ImagePlus object. */ + public static ImagePlus openImage() { + return openImage(null); + } + + /** Opens a URL and returns the contents as a string. + Returns "" if there an error, including + host or file not found. */ + public static String openUrlAsString(String url) { + //if (!trustManagerCreated && url.contains("nih.gov")) trustAllCerts(); + url = Opener.updateUrl(url); + if (debugMode) log("OpenUrlAsString: "+url); + StringBuffer sb = null; + url = url.replaceAll(" ", "%20"); + try { + //if (url.contains("nih.gov")) addRootCA(); + URL u = new URL(url); + URLConnection uc = u.openConnection(); + long len = uc.getContentLength(); + if (len>5242880L) + return ""; + InputStream in = u.openStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + sb = new StringBuffer() ; + String line; + while ((line=br.readLine()) != null) + sb.append (line + "\n"); + in.close (); + } catch (Exception e) { + return(""); + } + if (sb!=null) + return new String(sb); + else + return ""; + } + + /* + public static void addRootCA() throws Exception { + String path = "/Users/wayne/Downloads/Certificates/lets-encrypt-x1-cross-signed.pem"; + InputStream fis = new BufferedInputStream(new FileInputStream(path)); + Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(fis); + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null, null); + ks.setCertificateEntry(Integer.toString(1), ca); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, tmf.getTrustManagers(), null); + HttpsURLConnection.setDefaultSSLSocketFactory(ctx.getSocketFactory()); + } + */ + + /* + // Create a new trust manager that trust all certificates + // http://stackoverflow.com/questions/10135074/download-file-from-https-server-using-java + private static void trustAllCerts() { + trustManagerCreated = true; + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + public void checkClientTrusted (java.security.cert.X509Certificate[] certs, String authType) { + } + public void checkServerTrusted (java.security.cert.X509Certificate[] certs, String authType) { + } + } + }; + // Activate the new trust manager + try { + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } catch (Exception e) { + IJ.log(""+e); + } + } + */ + + /** Saves the current image, lookup table, selection or text window to the specified file path. + The path must end in ".tif", ".jpg", ".gif", ".zip", ".raw", ".avi", ".bmp", ".fits", ".pgm", ".png", ".lut", ".roi" or ".txt". */ + public static void save(String path) { + save(null, path); + } + + /** Saves the specified image, lookup table or selection to the specified file path. + The file path should end with ".tif", ".jpg", ".gif", ".zip", ".raw", ".avi", ".bmp", + ".fits", ".pgm", ".png", ".lut", ".roi" or ".txt". The specified image is saved in + TIFF format if there is no extension. */ + public static void save(ImagePlus imp, String path) { + ImagePlus imp2 = imp; + if (imp2==null) + imp2 = WindowManager.getCurrentImage(); + int dotLoc = path.lastIndexOf('.'); + if (dotLoc==-1 && imp2!=null) { + path = path + ".tif"; // save as TIFF if file name does not have an extension + dotLoc = path.lastIndexOf('.'); + } + if (dotLoc!=-1) { + String title = imp2!=null?imp2.getTitle():null; + saveAs(imp, path.substring(dotLoc+1), path); + if (title!=null) + imp2.setTitle(title); + } else + error("The file path passed to IJ.save() method or save()\nmacro function is missing the required extension.\n \n\""+path+"\""); + } + + /* Saves the active image, lookup table, selection, measurement results, selection XY + coordinates or text window to the specified file path. The format argument must be "tiff", + "jpeg", "gif", "zip", "raw", "avi", "bmp", "fits", "pgm", "png", "text image", "lut", "selection", "measurements", + "xy Coordinates" or "text". If path is null or an emply string, a file + save dialog is displayed. */ + public static void saveAs(String format, String path) { + saveAs(null, format, path); + } + + /* Saves the specified image. The format argument must be "tiff", + "jpeg", "gif", "zip", "raw", "avi", "bmp", "fits", "pgm", "png", + "text image", "lut", "selection" or "xy Coordinates". */ + public static void saveAs(ImagePlus imp, String format, String path) { + if (format==null) + return; + if (path!=null && path.length()==0) + path = null; + format = format.toLowerCase(Locale.US); + Roi roi2 = imp!=null?imp.getRoi():null; + if (roi2!=null) + roi2.endPaste(); + if (format.indexOf("tif")!=-1) { + saveAsTiff(imp, path); + return; + } else if (format.indexOf("jpeg")!=-1 || format.indexOf("jpg")!=-1) { + path = updateExtension(path, ".jpg"); + JpegWriter.save(imp, path, FileSaver.getJpegQuality()); + return; + } else if (format.indexOf("gif")!=-1) { + path = updateExtension(path, ".gif"); + GifWriter.save(imp, path); + return; + } else if (format.indexOf("text image")!=-1) { + path = updateExtension(path, ".txt"); + format = "Text Image..."; + } else if (format.indexOf("text")!=-1 || format.indexOf("txt")!=-1) { + if (path!=null && !path.endsWith(".xls") && !path.endsWith(".csv") && !path.endsWith(".tsv")) + path = updateExtension(path, ".txt"); + format = "Text..."; + } else if (format.indexOf("zip")!=-1) { + path = updateExtension(path, ".zip"); + format = "ZIP..."; + } else if (format.indexOf("raw")!=-1) { + //path = updateExtension(path, ".raw"); + format = "Raw Data..."; + } else if (format.indexOf("avi")!=-1) { + path = updateExtension(path, ".avi"); + format = "AVI... "; + } else if (format.indexOf("bmp")!=-1) { + path = updateExtension(path, ".bmp"); + format = "BMP..."; + } else if (format.indexOf("fits")!=-1) { + path = updateExtension(path, ".fits"); + format = "FITS..."; + } else if (format.indexOf("png")!=-1) { + path = updateExtension(path, ".png"); + format = "PNG..."; + } else if (format.indexOf("pgm")!=-1) { + path = updateExtension(path, ".pgm"); + format = "PGM..."; + } else if (format.indexOf("lut")!=-1) { + path = updateExtension(path, ".lut"); + format = "LUT..."; + } else if (format.contains("results") || format.contains("measurements") || format.contains("table")) { + format = "Results..."; + } else if (format.contains("selection") || format.contains("roi")) { + path = updateExtension(path, ".roi"); + format = "Selection..."; + } else if (format.indexOf("xy")!=-1 || format.indexOf("coordinates")!=-1) { + path = updateExtension(path, ".txt"); + format = "XY Coordinates..."; + } else + error("Unsupported save() or saveAs() file format: \""+format+"\"\n \n\""+path+"\""); + if (path==null) + run(format); + else { + if (path.contains(" ")) + run(imp, format, "save=["+path+"]"); + else + run(imp, format, "save="+path); + } + } + + /** Saves the specified image in TIFF format. Displays a file save dialog + if 'path' is null or an empty string. Returns 'false' if there is an + error or if the user selects "Cancel" in the file save dialog. */ + public static boolean saveAsTiff(ImagePlus imp, String path) { + if (imp==null) + imp = getImage(); + if (path==null || path.equals("")) + return (new FileSaver(imp)).saveAsTiff(); + if (!path.endsWith(".tiff")) + path = updateExtension(path, ".tif"); + FileSaver fs = new FileSaver(imp); + boolean ok; + if (imp.getStackSize()>1) + ok = fs.saveAsTiffStack(path); + else + ok = fs.saveAsTiff(path); + if (ok) + fs.updateImagePlus(path, FileInfo.TIFF); + return ok; + } + + static String updateExtension(String path, String extension) { + if (path==null) return null; + int dotIndex = path.lastIndexOf("."); + int separatorIndex = path.lastIndexOf(File.separator); + if (dotIndex>=0 && dotIndex>separatorIndex && (path.length()-dotIndex)<=5) { + if (dotIndex+1Type should contain "8-bit", "16-bit", "32-bit" or "RGB". + In addition, it can contain "white", "black" or "ramp". Width + and height specify the width and height of the image in pixels. + Depth specifies the number of stack slices. */ + public static ImagePlus createImage(String title, String type, int width, int height, int depth) { + type = type.toLowerCase(Locale.US); + int bitDepth = 8; + if (type.contains("16")) + bitDepth = 16; + boolean signedInt = type.contains("32-bit int"); + if (type.contains("32")) + bitDepth = 32; + if (type.contains("24") || type.contains("rgb") || signedInt) + bitDepth = 24; + int options = NewImage.FILL_WHITE; + if (bitDepth==16 || bitDepth==32) + options = NewImage.FILL_BLACK; + if (type.contains("white")) + options = NewImage.FILL_WHITE; + else if (type.contains("black")) + options = NewImage.FILL_BLACK; + else if (type.contains("ramp")) + options = NewImage.FILL_RAMP; + else if (type.contains("noise") || type.contains("random")) + options = NewImage.FILL_NOISE; + options += NewImage.CHECK_AVAILABLE_MEMORY; + if (signedInt) + options += NewImage.SIGNED_INT; + return NewImage.createImage(title, width, height, depth, bitDepth, options); + } + + /** Creates a new hyperstack. + * @param title image name + * @param type "8-bit", "16-bit", "32-bit" or "RGB". May also + * contain "white" , "black" (the default), "ramp", "composite-mode", + * "color-mode", "grayscale-mode or "label". + * @param width image width in pixels + * @param height image height in pixels + * @param channels number of channels + * @param slices number of slices + * @param frames number of frames + */ + public static ImagePlus createImage(String title, String type, int width, int height, int channels, int slices, int frames) { + if (type.contains("label")) + type += "ramp"; + if (!(type.contains("white")||type.contains("ramp"))) + type += "black"; + ImagePlus imp = IJ.createImage(title, type, width, height, channels*slices*frames); + imp.setDimensions(channels, slices, frames); + int mode = IJ.COLOR; + if (type.contains("composite")) + mode = IJ.COMPOSITE; + if (type.contains("grayscale")) + mode = IJ.GRAYSCALE; + if (channels>1 && imp.getBitDepth()!=24) + imp = new CompositeImage(imp, mode); + imp.setOpenAsHyperStack(true); + if (type.contains("label")) + HyperStackMaker.labelHyperstack(imp); + return imp; + } + + /** Creates a new hyperstack. + * @param title image name + * @param width image width in pixels + * @param height image height in pixels + * @param channels number of channels + * @param slices number of slices + * @param frames number of frames + * @param bitdepth 8, 16, 32 (float) or 24 (RGB) + */ + public static ImagePlus createHyperStack(String title, int width, int height, int channels, int slices, int frames, int bitdepth) { + ImagePlus imp = createImage(title, width, height, channels*slices*frames, bitdepth); + imp.setDimensions(channels, slices, frames); + if (channels>1 && bitdepth!=24) + imp = new CompositeImage(imp, IJ.COMPOSITE); + imp.setOpenAsHyperStack(true); + return imp; + } + + /** Opens a new image. Type should contain "8-bit", "16-bit", "32-bit" or "RGB". + In addition, it can contain "white", "black" or "ramp". Width + and height specify the width and height of the image in pixels. + Depth specifies the number of stack slices. */ + public static void newImage(String title, String type, int width, int height, int depth) { + ImagePlus imp = createImage(title, type, width, height, depth); + if (imp!=null) { + macroRunning = true; + imp.show(); + macroRunning = false; + } + } + + /** Returns true if the Esc key was pressed since the + last ImageJ command started to execute or since resetEscape() was called. */ + public static boolean escapePressed() { + return escapePressed; + } + + /** This method sets the Esc key to the "up" position. + The Executer class calls this method when it runs + an ImageJ command in a separate thread. */ + public static void resetEscape() { + escapePressed = false; + } + + /** Causes IJ.error() output to be temporarily redirected to the "Log" window. */ + public static void redirectErrorMessages() { + redirectErrorMessages = true; + lastErrorMessage = null; + } + + /** Set 'true' and IJ.error() output will be temporarily redirected to the "Log" window. */ + public static void redirectErrorMessages(boolean redirect) { + redirectErrorMessages = redirect; + lastErrorMessage = null; + } + + /** Returns the state of the 'redirectErrorMessages' flag, which is set by File/Import/Image Sequence. */ + public static boolean redirectingErrorMessages() { + return redirectErrorMessages; + } + + /** Temporarily suppress "plugin not found" errors. */ + public static void suppressPluginNotFoundError() { + suppressPluginNotFoundError = true; + } + + /** Returns the class loader ImageJ uses to run plugins or the + system class loader if Menus.getPlugInsPath() returns null. */ + public static ClassLoader getClassLoader() { + if (classLoader==null) { + String pluginsDir = Menus.getPlugInsPath(); + if (pluginsDir==null) { + String home = System.getProperty("plugins.dir"); + if (home!=null) { + if (!home.endsWith(Prefs.separator)) home+=Prefs.separator; + pluginsDir = home+"plugins"+Prefs.separator; + if (!(new File(pluginsDir)).isDirectory()) + pluginsDir = home; + } + } + if (pluginsDir==null) + return IJ.class.getClassLoader(); + else { + if (Menus.jnlp) + classLoader = new PluginClassLoader(pluginsDir, true); + else + classLoader = new PluginClassLoader(pluginsDir); + } + } + return classLoader; + } + + /** Returns the size, in pixels, of the primary display. */ + public static Dimension getScreenSize() { + Rectangle bounds = GUI.getScreenBounds(); + return new Dimension(bounds.width, bounds.height); + } + + /** Returns, as an array of strings, a list of the LUTs in the + * Image/Lookup Tables menu. + * @see ij.plugin#LutLoader.getLut + * See also: Help>Examples>JavaScript/Show all LUTs + * and Image/Color/Display LUTs + */ + public static String[] getLuts() { + ArrayList list = new ArrayList(); + Hashtable commands = Menus.getCommands(); + Menu lutsMenu = Menus.getImageJMenu("Image>Lookup Tables"); + if (commands==null || lutsMenu==null) + return new String[0]; + for (int i=0; i +ImageJ is a work of the United States Government. It is in the public domain +and open source. There is no copyright. You are free to do anything you want +with this source but I like to get credit for my work and I would like you to +offer your changes to me so I can possibly add them to the "official" version. + +
+The following command line options are recognized by ImageJ:
+
+  "file-name"
+     Opens a file
+     Example 1: blobs.tif
+     Example 2: /Users/wayne/images/blobs.tif
+     Example 3: e81*.tif
+
+  -macro path [arg]
+     Runs a macro or script (JavaScript, BeanShell or Python), passing an
+     optional string argument, which the macro or script can be retrieve
+     using the getArgument() function. The macro or script is assumed to 
+     be in the ImageJ/macros folder if 'path' is not a full directory path.
+     Example 1: -macro analyze.ijm
+     Example 2: -macro script.js /Users/wayne/images/stack1
+     Example 2: -macro script.py '1.2 2.4 3.8'
+
+  -batch path [arg]
+    Runs a macro or script (JavaScript, BeanShell or Python) in
+    batch (no GUI) mode, passing an optional argument.
+    ImageJ exits when the macro finishes.
+
+  -eval "macro code"
+     Evaluates macro code
+     Example 1: -eval "print('Hello, world');"
+     Example 2: -eval "return getVersion();"
+
+  -run command
+     Runs an ImageJ menu command
+     Example: -run "About ImageJ..."
+     
+  -ijpath path
+     Specifies the path to the directory containing the plugins directory
+     Example: -ijpath /Applications/ImageJ
+
+  -port
+     Specifies the port ImageJ uses to determine if another instance is running
+     Example 1: -port1 (use default port address + 1)
+     Example 2: -port2 (use default port address + 2)
+     Example 3: -port0 (don't check for another instance)
+
+  -debug
+     Runs ImageJ in debug mode
+
+@author Wayne Rasband (rasband@gmail.com) +*/ +public class ImageJ extends Frame implements ActionListener, + MouseListener, KeyListener, WindowListener, ItemListener, Runnable { + + /** Plugins should call IJ.getVersion() or IJ.getFullVersion() to get the version string. */ + public static final String VERSION = "1.53n"; + public static final String BUILD = ""; //28 + public static Color backgroundColor = new Color(237,237,237); + /** SansSerif, 12-point, plain font. */ + public static final Font SansSerif12 = new Font("SansSerif", Font.PLAIN, 12); + /** Address of socket where Image accepts commands */ + public static final int DEFAULT_PORT = 57294; + + /** Run as normal application. */ + public static final int STANDALONE = 0; + + /** Run embedded in another application. */ + public static final int EMBEDDED = 1; + + /** Run embedded and invisible in another application. */ + public static final int NO_SHOW = 2; + + /** Run ImageJ in debug mode. */ + public static final int DEBUG = 256; + + private static final String IJ_X="ij.x",IJ_Y="ij.y"; + private static int port = DEFAULT_PORT; + private static String[] arguments; + + private Toolbar toolbar; + private Panel statusBar; + private ProgressBar progressBar; + private JLabel statusLine; + private boolean firstTime = true; + private java.applet.Applet applet; // null if not running as an applet + private Vector classes = new Vector(); + private boolean exitWhenQuitting; + private boolean quitting; + private boolean quitMacro; + private long keyPressedTime, actionPerformedTime; + private String lastKeyCommand; + private boolean embedded; + private boolean windowClosed; + private static String commandName; + private static boolean batchMode; + + boolean hotkey; + + /** Creates a new ImageJ frame that runs as an application. */ + public ImageJ() { + this(null, STANDALONE); + } + + /** Creates a new ImageJ frame that runs as an application in the specified mode. */ + public ImageJ(int mode) { + this(null, mode); + } + + /** Creates a new ImageJ frame that runs as an applet. */ + public ImageJ(java.applet.Applet applet) { + this(applet, STANDALONE); + } + + /** If 'applet' is not null, creates a new ImageJ frame that runs as an applet. + If 'mode' is ImageJ.EMBEDDED and 'applet is null, creates an embedded + (non-standalone) version of ImageJ. */ + public ImageJ(java.applet.Applet applet, int mode) { + super("ImageJ"); + if ((mode&DEBUG)!=0) + IJ.setDebugMode(true); + mode = mode & 255; + if (IJ.debugMode) IJ.log("ImageJ starting in debug mode: "+mode); + embedded = applet==null && (mode==EMBEDDED||mode==NO_SHOW); + this.applet = applet; + String err1 = Prefs.load(this, applet); + setBackground(backgroundColor); + Menus m = new Menus(this, applet); + String err2 = m.addMenuBar(); + m.installPopupMenu(this); + setLayout(new BorderLayout()); + + // Tool bar + toolbar = new Toolbar(); + toolbar.addKeyListener(this); + add("Center", toolbar); + + // Status bar + statusBar = new Panel(); + statusBar.setLayout(new BorderLayout()); + statusBar.setForeground(Color.black); + statusBar.setBackground(backgroundColor); + statusLine = new JLabel(); + double scale = Prefs.getGuiScale(); + statusLine.setFont(new Font("SansSerif", Font.PLAIN, (int)(13*scale))); + statusLine.addKeyListener(this); + statusLine.addMouseListener(this); + statusBar.add("Center", statusLine); + progressBar = new ProgressBar((int)(ProgressBar.WIDTH*scale), (int)(ProgressBar.HEIGHT*scale)); + progressBar.addKeyListener(this); + progressBar.addMouseListener(this); + statusBar.add("East", progressBar); + add("South", statusBar); + + IJ.init(this, applet); + addKeyListener(this); + addWindowListener(this); + setFocusTraversalKeysEnabled(false); + m.installStartupMacroSet(); //add custom tools + + Point loc = getPreferredLocation(); + Dimension tbSize = toolbar.getPreferredSize(); + setCursor(Cursor.getDefaultCursor()); // work-around for JDK 1.1.8 bug + if (mode!=NO_SHOW) { + if (IJ.isWindows()) try {setIcon();} catch(Exception e) {} + setResizable(false); + setAlwaysOnTop(Prefs.alwaysOnTop); + pack(); + setLocation(loc.x, loc.y); + setVisible(true); + Dimension size = getSize(); + if (size!=null) { + if (IJ.debugMode) IJ.log("size: "+size); + if (IJ.isWindows() && (size.height>108||IJ.javaVersion()>=10)) { + // workaround for IJ window layout and FileDialog freeze problems with Windows 10 Creators Update + IJ.wait(10); + pack(); + if (IJ.debugMode) IJ.log("pack()"); + if (!Prefs.jFileChooserSettingChanged) + Prefs.useJFileChooser = true; + } else if (IJ.isMacOSX()) { + Rectangle maxBounds = GUI.getMaxWindowBounds(this); + if (loc.x+size.width>maxBounds.x+maxBounds.width) + setLocation(loc.x, loc.y); + } + } + } + if (err1!=null) + IJ.error(err1); + if (err2!=null) { + IJ.error(err2); + IJ.runPlugIn("ij.plugin.ClassChecker", ""); + } + if (IJ.isMacintosh()&&applet==null) { + try { + if (IJ.javaVersion()>8) // newer JREs use different drag-drop, about mechanism + IJ.runPlugIn("ij.plugin.MacAdapter9", ""); + else + IJ.runPlugIn("ij.plugin.MacAdapter", ""); + } catch(Throwable e) {} + } + if (applet==null) + IJ.runPlugIn("ij.plugin.DragAndDrop", ""); + if (!getTitle().contains("Fiji")) { + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler()); + System.setProperty("sun.awt.exception.handler",ExceptionHandler.class.getName()); + } + String str = m.getMacroCount()==1?" macro":" macros"; + configureProxy(); + if (applet==null) + loadCursors(); + (new ij.macro.StartupRunner()).run(batchMode); // run RunAtStartup and AutoRun macros + IJ.showStatus(version()+ m.getPluginCount() + " commands; " + m.getMacroCount() + str); + } + + private void loadCursors() { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + String path = Prefs.getImageJDir()+"images/crosshair-cursor.gif"; + File f = new File(path); + if (!f.exists()) + return; + //Image image = toolkit.getImage(path); + ImageIcon icon = new ImageIcon(path); + Image image = icon.getImage(); + if (image==null) + return; + int width = icon.getIconWidth(); + int height = icon.getIconHeight(); + Point hotSpot = new Point(width/2, height/2); + Cursor crosshairCursor = toolkit.createCustomCursor(image, hotSpot, "crosshair-cursor.gif"); + ImageCanvas.setCursor(crosshairCursor, 0); + } + + void configureProxy() { + if (Prefs.useSystemProxies) { + try { + System.setProperty("java.net.useSystemProxies", "true"); + } catch(Exception e) {} + } else { + String server = Prefs.get("proxy.server", null); + if (server==null||server.equals("")) + return; + int port = (int)Prefs.get("proxy.port", 0); + if (port==0) return; + Properties props = System.getProperties(); + props.put("proxySet", "true"); + props.put("http.proxyHost", server); + props.put("http.proxyPort", ""+port); + props.put("https.proxyHost", server); + props.put("https.proxyPort", ""+port); + } + //new ProxySettings().logProperties(); + } + + void setIcon() throws Exception { + URL url = this.getClass().getResource("/microscope.gif"); + if (url==null) return; + Image img = createImage((ImageProducer)url.getContent()); + if (img!=null) setIconImage(img); + } + + public Point getPreferredLocation() { + int ijX = Prefs.getInt(IJ_X,-99); + int ijY = Prefs.getInt(IJ_Y,-99); + Rectangle maxBounds = GUI.getMaxWindowBounds(); + //System.out.println("getPreferredLoc1: "+ijX+" "+ijY+" "+maxBounds); + if (ijX>=maxBounds.x && ijY>=maxBounds.y && ijX<(maxBounds.x+maxBounds.width-75) + && ijY<(maxBounds.y+maxBounds.height-75)) + return new Point(ijX, ijY); + Dimension tbsize = toolbar.getPreferredSize(); + int ijWidth = tbsize.width+10; + double percent = maxBounds.width>832?0.8:0.9; + ijX = (int)(percent*(maxBounds.width-ijWidth)); + if (ijX<10) ijX = 10; + return new Point(ijX, maxBounds.y); + } + + void showStatus(String s) { + statusLine.setText(s); + } + + public ProgressBar getProgressBar() { + return progressBar; + } + + public Panel getStatusBar() { + return statusBar; + } + + /** Starts executing a menu command in a separate thread. */ + void doCommand(String name) { + new Executer(name, null); + } + + public void runFilterPlugIn(Object theFilter, String cmd, String arg) { + new PlugInFilterRunner(theFilter, cmd, arg); + } + + public Object runUserPlugIn(String commandName, String className, String arg, boolean createNewLoader) { + return IJ.runUserPlugIn(commandName, className, arg, createNewLoader); + } + + /** Return the current list of modifier keys. */ + public static String modifiers(int flags) { //?? needs to be moved + String s = " [ "; + if (flags == 0) return ""; + if ((flags & Event.SHIFT_MASK) != 0) s += "Shift "; + if ((flags & Event.CTRL_MASK) != 0) s += "Control "; + if ((flags & Event.META_MASK) != 0) s += "Meta "; + if ((flags & Event.ALT_MASK) != 0) s += "Alt "; + s += "] "; + return s; + } + + /** Handle menu events. */ + public void actionPerformed(ActionEvent e) { + if ((e.getSource() instanceof MenuItem)) { + MenuItem item = (MenuItem)e.getSource(); + String cmd = e.getActionCommand(); + Frame frame = WindowManager.getFrontWindow(); + if (frame!=null && (frame instanceof Fitter)) { + ((Fitter)frame).actionPerformed(e); + return; + } + commandName = cmd; + ImagePlus imp = null; + if (item.getParent()==Menus.getOpenRecentMenu()) { + new RecentOpener(cmd); // open image in separate thread + return; + } else if (item.getParent()==Menus.getPopupMenu()) { + Object parent = Menus.getPopupMenu().getParent(); + if (parent instanceof ImageCanvas) + imp = ((ImageCanvas)parent).getImage(); + } + int flags = e.getModifiers(); + hotkey = false; + actionPerformedTime = System.currentTimeMillis(); + long ellapsedTime = actionPerformedTime-keyPressedTime; + if (cmd!=null && (ellapsedTime>=200L||!cmd.equals(lastKeyCommand))) { + if ((flags & Event.ALT_MASK)!=0) + IJ.setKeyDown(KeyEvent.VK_ALT); + if ((flags & Event.SHIFT_MASK)!=0) + IJ.setKeyDown(KeyEvent.VK_SHIFT); + new Executer(cmd, imp); + } + lastKeyCommand = null; + if (IJ.debugMode) IJ.log("actionPerformed: time="+ellapsedTime+", "+e); + } + } + + /** Handles CheckboxMenuItem state changes. */ + public void itemStateChanged(ItemEvent e) { + MenuItem item = (MenuItem)e.getSource(); + MenuComponent parent = (MenuComponent)item.getParent(); + String cmd = e.getItem().toString(); + if ("Autorun Examples".equals(cmd)) // Examples>Autorun Examples + Prefs.autoRunExamples = e.getStateChange()==1; + else if ((Menu)parent==Menus.window) + WindowManager.activateWindow(cmd, item); + else + doCommand(cmd); + } + + public void mousePressed(MouseEvent e) { + Undo.reset(); + if (!Prefs.noClickToGC) + System.gc(); + IJ.showStatus(version()+IJ.freeMemory()); + if (IJ.debugMode) + IJ.log("Windows: "+WindowManager.getWindowCount()); + } + + public String getInfo() { + return version()+System.getProperty("os.name")+" "+System.getProperty("os.version")+"; "+IJ.freeMemory(); + } + + private String version() { + return "ImageJ "+VERSION+BUILD + "; "+"Java "+System.getProperty("java.version")+(IJ.is64Bit()?" [64-bit]; ":" [32-bit]; "); + } + + public void mouseReleased(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + public void mouseClicked(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + + public void keyPressed(KeyEvent e) { + if (e.isConsumed()) + return; + int keyCode = e.getKeyCode(); + IJ.setKeyDown(keyCode); + hotkey = false; + if (keyCode==KeyEvent.VK_CONTROL || keyCode==KeyEvent.VK_SHIFT) + return; + char keyChar = e.getKeyChar(); + int flags = e.getModifiers(); + if (IJ.debugMode) IJ.log("keyPressed: code=" + keyCode + " (" + KeyEvent.getKeyText(keyCode) + + "), char=\"" + keyChar + "\" (" + (int)keyChar + "), flags=" + + KeyEvent.getKeyModifiersText(flags)); + boolean shift = (flags & KeyEvent.SHIFT_MASK) != 0; + boolean control = (flags & KeyEvent.CTRL_MASK) != 0; + boolean alt = (flags & KeyEvent.ALT_MASK) != 0; + boolean meta = (flags & KeyEvent.META_MASK) != 0; + if (keyCode==KeyEvent.VK_H && meta && IJ.isMacOSX()) + return; // Allow macOS to run ImageJ>Hide ImageJ command + String cmd = null; + ImagePlus imp = WindowManager.getCurrentImage(); + boolean isStack = (imp!=null) && (imp.getStackSize()>1); + + if (imp!=null && !meta && ((keyChar>=32 && keyChar<=255) || keyChar=='\b' || keyChar=='\n')) { + Roi roi = imp.getRoi(); + if (roi!=null && roi instanceof TextRoi) { + if (imp.getOverlay()!=null && (control || alt || meta) + && (keyCode==KeyEvent.VK_BACK_SPACE || keyCode==KeyEvent.VK_DELETE)) { + if (deleteOverlayRoi(imp)) + return; + } + if ((flags & KeyEvent.META_MASK)!=0 && IJ.isMacOSX()) + return; + if (alt) { + switch (keyChar) { + case 'u': case 'm': keyChar = IJ.micronSymbol; break; + case 'A': keyChar = IJ.angstromSymbol; break; + default: + } + } + ((TextRoi)roi).addChar(keyChar); + return; + } + } + + // Handle one character macro shortcuts + if (!control && !meta) { + Hashtable macroShortcuts = Menus.getMacroShortcuts(); + if (macroShortcuts.size()>0) { + if (shift) + cmd = (String)macroShortcuts.get(new Integer(keyCode+200)); + else + cmd = (String)macroShortcuts.get(new Integer(keyCode)); + if (cmd!=null) { + commandName = cmd; + MacroInstaller.runMacroShortcut(cmd); + return; + } + } + } + + if (keyCode==KeyEvent.VK_SEPARATOR) + keyCode = KeyEvent.VK_DECIMAL; + boolean functionKey = keyCode>=KeyEvent.VK_F1 && keyCode<=KeyEvent.VK_F12; + boolean numPad = keyCode==KeyEvent.VK_DIVIDE || keyCode==KeyEvent.VK_MULTIPLY + || keyCode==KeyEvent.VK_DECIMAL + || (keyCode>=KeyEvent.VK_NUMPAD0 && keyCode<=KeyEvent.VK_NUMPAD9); + if ((!Prefs.requireControlKey||control||meta||functionKey||numPad) && keyChar!='+') { + Hashtable shortcuts = Menus.getShortcuts(); + if (shift && !functionKey) + cmd = (String)shortcuts.get(new Integer(keyCode+200)); + else + cmd = (String)shortcuts.get(new Integer(keyCode)); + } + + if (cmd==null) { + switch (keyChar) { + case '<': case ',': if (isStack) cmd="Previous Slice [<]"; break; + case '>': case '.': case ';': if (isStack) cmd="Next Slice [>]"; break; + case '+': case '=': cmd="In [+]"; break; + case '-': cmd="Out [-]"; break; + case '/': cmd="Reslice [/]..."; break; + default: + } + } + + if (cmd==null) { + switch (keyCode) { + case KeyEvent.VK_TAB: WindowManager.putBehind(); return; + case KeyEvent.VK_BACK_SPACE: case KeyEvent.VK_DELETE: + if (!(shift||control||alt||meta)) { + if (deleteOverlayRoi(imp)) + return; + if (imp!=null&&imp.getOverlay()!=null&&imp==GelAnalyzer.getGelImage()) + return; + cmd="Clear"; + hotkey=true; + } + break; + //case KeyEvent.VK_BACK_SLASH: cmd=IJ.altKeyDown()?"Animation Options...":"Start Animation"; break; + case KeyEvent.VK_EQUALS: cmd="In [+]"; break; + case KeyEvent.VK_MINUS: cmd="Out [-]"; break; + case KeyEvent.VK_SLASH: case 0xbf: cmd="Reslice [/]..."; break; + case KeyEvent.VK_COMMA: case 0xbc: if (isStack) cmd="Previous Slice [<]"; break; + case KeyEvent.VK_PERIOD: case 0xbe: if (isStack) cmd="Next Slice [>]"; break; + case KeyEvent.VK_LEFT: case KeyEvent.VK_RIGHT: case KeyEvent.VK_UP: case KeyEvent.VK_DOWN: // arrow keys + if (imp==null) return; + Roi roi = imp.getRoi(); + if (shift&&imp==Orthogonal_Views.getImage()) + return; + if (IJ.isMacOSX() && IJ.isJava18()) { + RoiManager rm = RoiManager.getInstance(); + boolean rmActive = rm!=null && rm==WindowManager.getActiveWindow(); + if (rmActive && (keyCode==KeyEvent.VK_DOWN||keyCode==KeyEvent.VK_UP)) + rm.repaint(); + } + boolean stackKey = imp.getStackSize()>1 && (roi==null||shift); + boolean zoomKey = roi==null || shift || control; + if (stackKey && keyCode==KeyEvent.VK_RIGHT) + cmd="Next Slice [>]"; + else if (stackKey && keyCode==KeyEvent.VK_LEFT) + cmd="Previous Slice [<]"; + else if (zoomKey && keyCode==KeyEvent.VK_DOWN && !ignoreArrowKeys(imp) && Toolbar.getToolId()1 && win!=null && win.getClass().getName().startsWith("loci")) + return true; + return false; + } + + public void keyTyped(KeyEvent e) { + char keyChar = e.getKeyChar(); + int flags = e.getModifiers(); + //if (IJ.debugMode) IJ.log("keyTyped: char=\"" + keyChar + "\" (" + (int)keyChar + // + "), flags= "+Integer.toHexString(flags)+ " ("+KeyEvent.getKeyModifiersText(flags)+")"); + if (keyChar=='\\' || keyChar==171 || keyChar==223) { + if (((flags&Event.ALT_MASK)!=0)) + doCommand("Animation Options..."); + else + doCommand("Start Animation [\\]"); + } + } + + public void keyReleased(KeyEvent e) { + IJ.setKeyUp(e.getKeyCode()); + } + + /** called when escape pressed */ + void abortPluginOrMacro(ImagePlus imp) { + if (imp!=null) { + ImageWindow win = imp.getWindow(); + if (win!=null) { + Roi roi = imp.getRoi(); + if (roi!=null && roi.getState()!=Roi.NORMAL) { + roi.abortModification(imp); + return; + } else { + win.running = false; + win.running2 = false; + } + } + } + Macro.abort(); + Interpreter.abort(); + if (Interpreter.getInstance()!=null) + IJ.beep(); + } + + public void windowClosing(WindowEvent e) { + if (Executer.getListenerCount()>0) + doCommand("Quit"); + else { + quit(); + windowClosed = true; + } + } + + public void windowActivated(WindowEvent e) { + if (IJ.isMacintosh() && !quitting) { + IJ.wait(10); // may be needed for Java 1.4 on OS X + MenuBar mb = Menus.getMenuBar(); + if (mb!=null && mb!=getMenuBar()) { + setMenuBar(mb); + Menus.setMenuBarCount++; + if (IJ.debugMode) IJ.log("setMenuBar: "+Menus.setMenuBarCount); + } + } + } + + public void windowClosed(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + + /** Adds the specified class to a Vector to keep it from being + garbage collected, causing static fields to be reset. */ + public void register(Class c) { + if (!classes.contains(c)) + classes.addElement(c); + } + + /** Called by ImageJ when the user selects Quit. */ + public void quit() { + quitMacro = IJ.macroRunning(); + Thread thread = new Thread(this, "Quit"); + thread.setPriority(Thread.NORM_PRIORITY); + thread.start(); + IJ.wait(10); + } + + /** Returns true if ImageJ is exiting. */ + public boolean quitting() { + return quitting; + } + + /** Returns true if ImageJ is quitting as a result of a run("Quit") macro call. */ + public boolean quittingViaMacro() { + return quitting && quitMacro; + } + + /** Called once when ImageJ quits. */ + public void savePreferences(Properties prefs) { + Point loc = getLocation(); + prefs.put(IJ_X, Integer.toString(loc.x)); + prefs.put(IJ_Y, Integer.toString(loc.y)); + } + + public static void main(String args[]) { + boolean noGUI = false; + int mode = STANDALONE; + arguments = args; + int nArgs = args!=null?args.length:0; + boolean commandLine = false; + for (int i=0; i0 && DEFAULT_PORT+delta<65536) + port = DEFAULT_PORT+delta; + } + } + // If existing ImageJ instance, pass arguments to it and quit. + boolean passArgs = mode==STANDALONE && !noGUI; + if (IJ.isMacOSX() && !commandLine) + passArgs = false; + if (passArgs && isRunning(args)) + return; + ImageJ ij = IJ.getInstance(); + if (!noGUI && (ij==null || (ij!=null && !ij.isShowing()))) { + ij = new ImageJ(null, mode); + ij.exitWhenQuitting = true; + } else if (batchMode && noGUI) + Prefs.load(null, null); + int macros = 0; + for (int i=0; i0 && arg.indexOf("ij.ImageJ")==-1) { + File file = new File(arg); + IJ.open(file.getAbsolutePath()); + } + } + if (IJ.debugMode && IJ.getInstance()==null) + new JavaProperties().run(""); + if (noGUI) System.exit(0); + } + + // Is there another instance of ImageJ? If so, send it the arguments and quit. + static boolean isRunning(String args[]) { + return OtherInstance.sendArguments(args); + } + + /** Returns the port that ImageJ checks on startup to see if another instance is running. + * @see ij.OtherInstance + */ + public static int getPort() { + return port; + } + + /** Returns the command line arguments passed to ImageJ. */ + public static String[] getArgs() { + return arguments; + } + + /** ImageJ calls System.exit() when qutting when 'exitWhenQuitting' is true.*/ + public void exitWhenQuitting(boolean ewq) { + exitWhenQuitting = ewq; + } + + /** Quit using a separate thread, hopefully avoiding thread deadlocks. */ + public void run() { + quitting = true; + boolean changes = false; + int[] wList = WindowManager.getIDList(); + if (wList!=null) { + for (int i=0; iMenus.WINDOW_MENU_ITEMS && !(IJ.macroRunning()&&WindowManager.getImageCount()==0)) { + GenericDialog gd = new GenericDialog("ImageJ", this); + gd.addMessage("Are you sure you want to quit ImageJ?"); + gd.showDialog(); + quitting = !gd.wasCanceled(); + windowClosed = false; + } + if (!quitting) + return; + if (!WindowManager.closeAllWindows()) { + quitting = false; + return; + } + if (applet==null) { + saveWindowLocations(); + Prefs.set(ImageWindow.LOC_KEY,null); // don't save image window location + Prefs.savePreferences(); + } + IJ.cleanup(); + dispose(); + if (exitWhenQuitting) + System.exit(0); + } + + void saveWindowLocations() { + Window win = WindowManager.getWindow("B&C"); + if (win!=null) + Prefs.saveLocation(ContrastAdjuster.LOC_KEY, win.getLocation()); + win = WindowManager.getWindow("Threshold"); + if (win!=null) + Prefs.saveLocation(ThresholdAdjuster.LOC_KEY, win.getLocation()); + win = WindowManager.getWindow("Results"); + if (win!=null) { + Prefs.saveLocation(TextWindow.LOC_KEY, win.getLocation()); + Dimension d = win.getSize(); + Prefs.set(TextWindow.WIDTH_KEY, d.width); + Prefs.set(TextWindow.HEIGHT_KEY, d.height); + } + win = WindowManager.getWindow("Log"); + if (win!=null) { + Prefs.saveLocation(TextWindow.LOG_LOC_KEY, win.getLocation()); + Dimension d = win.getSize(); + Prefs.set(TextWindow.LOG_WIDTH_KEY, d.width); + Prefs.set(TextWindow.LOG_HEIGHT_KEY, d.height); + } + win = WindowManager.getWindow("ROI Manager"); + if (win!=null) + Prefs.saveLocation(RoiManager.LOC_KEY, win.getLocation()); + } + + public static String getCommandName() { + return commandName!=null?commandName:"null"; + } + + public static void setCommandName(String name) { + commandName = name; + } + + public void resize() { + double scale = Prefs.getGuiScale(); + toolbar.init(); + statusLine.setFont(new Font("SansSerif", Font.PLAIN, (int)(13*scale))); + progressBar.init((int)(ProgressBar.WIDTH*scale), (int)(ProgressBar.HEIGHT*scale)); + pack(); + } + + /** Handles exceptions on the EDT. */ + public static class ExceptionHandler implements Thread.UncaughtExceptionHandler { + + // for EDT exceptions + public void handle(Throwable thrown) { + handleException(Thread.currentThread().getName(), thrown); + } + + // for other uncaught exceptions + public void uncaughtException(Thread thread, Throwable thrown) { + handleException(thread.getName(), thrown); + } + + protected void handleException(String tname, Throwable e) { + if (Macro.MACRO_CANCELED.equals(e.getMessage())) + return; + CharArrayWriter caw = new CharArrayWriter(); + PrintWriter pw = new PrintWriter(caw); + e.printStackTrace(pw); + String s = caw.toString(); + if (s!=null && s.contains("ij.")) { + if (IJ.getInstance()!=null) + s = IJ.getInstance().getInfo()+"\n"+s; + IJ.log(s); + } + } + + } // inner class ExceptionHandler + +} diff --git a/src/ij/ImageJApplet.java b/src/ij/ImageJApplet.java new file mode 100644 index 0000000..db5cb6e --- /dev/null +++ b/src/ij/ImageJApplet.java @@ -0,0 +1,44 @@ +package ij; +import java.applet.Applet; + +/** + Runs ImageJ as an applet and optionally opens up to + nine images using URLs passed as a parameters. +

+ Here is an example applet tag that launches ImageJ as an applet + and passes it the URLs of two images: +

+	<applet archive="../ij.jar" code="ij.ImageJApplet.class" width=0 height=0>
+	<param name=url1 value="http://imagej.nih.gov/ij/images/FluorescentCells.jpg">
+	<param name=url2 value="http://imagej.nih.gov/ij/images/blobs.gif">
+	</applet>
+	
+ To use plugins, add them to ij.jar and add entries to IJ_Props.txt file (in ij.jar) that will + create commands for them in the Plugins menu, or a submenu. There are examples + of such entries in IJ.Props.txt, in the "Plugins installed in the Plugins menu" section. +

+ Macros contained in a file named "StartupMacros.txt", in the same directory as the HTML file + containing the applet tag, will be installed on startup. +*/ +public class ImageJApplet extends Applet { + + /** Starts ImageJ if it's not already running. */ + public void init() { + ImageJ ij = IJ.getInstance(); + if (ij==null || (ij!=null && !ij.isShowing())) + new ImageJ(this); + for (int i=1; i<=9; i++) { + String url = getParameter("url"+i); + if (url==null) break; + ImagePlus imp = new ImagePlus(url); + if (imp!=null) imp.show(); + } + } + + public void destroy() { + ImageJ ij = IJ.getInstance(); + if (ij!=null) ij.quit(); + } + +} + diff --git a/src/ij/ImageListener.java b/src/ij/ImageListener.java new file mode 100644 index 0000000..ae8faac --- /dev/null +++ b/src/ij/ImageListener.java @@ -0,0 +1,17 @@ +package ij; + + /** Plugins that implement this interface are notified when + an image is opened, closed or updated. The + Plugins/Utilities/Monitor Events command uses this interface. + */ + public interface ImageListener { + + public void imageOpened(ImagePlus imp); + + public void imageClosed(ImagePlus imp); + + public void imageUpdated(ImagePlus imp); + + //default void imageSaved(ImagePlus imp) { } + +} diff --git a/src/ij/ImagePlus.java b/src/ij/ImagePlus.java new file mode 100644 index 0000000..231609e --- /dev/null +++ b/src/ij/ImagePlus.java @@ -0,0 +1,3438 @@ +package ij; +import java.awt.*; +import java.awt.image.*; +import java.net.URL; +import java.util.*; +import ij.process.*; +import ij.io.*; +import ij.gui.*; + +import ij.measure.*; +import ij.plugin.filter.Analyzer; +import ij.util.*; +import ij.macro.Interpreter; +import ij.plugin.*; +import ij.plugin.frame.*; + + +/** +An ImagePlus contain an ImageProcessor (2D image) or an ImageStack (3D, 4D or 5D image). +It also includes metadata (spatial calibration and possibly the directory/file where +it was read from). The ImageProcessor contains the pixel data (8-bit, 16-bit, float or RGB) +of the 2D image and some basic methods to manipulate it. An ImageStack is essentually +a list ImageProcessors of same type and size. +@see ij.process.ImageProcessor +@see ij.ImageStack +@see ij.gui.ImageWindow +@see ij.gui.ImageCanvas +*/ + +public class ImagePlus implements ImageObserver, Measurements, Cloneable { + + /** 8-bit grayscale (unsigned)*/ + public static final int GRAY8 = 0; + + /** 16-bit grayscale (unsigned) */ + public static final int GRAY16 = 1; + + /** 32-bit floating-point grayscale */ + public static final int GRAY32 = 2; + + /** 8-bit indexed color */ + public static final int COLOR_256 = 3; + + /** 32-bit RGB color */ + public static final int COLOR_RGB = 4; + + /** Title of image used by Flatten command */ + public static final String flattenTitle = "flatten~canvas"; + + /** True if any changes have been made to this image. */ + public boolean changes; + + protected Image img; + protected ImageProcessor ip; + protected ImageWindow win; + protected Roi roi; + protected int currentSlice; // current stack index (one-based) + protected static final int OPENED=0, CLOSED=1, UPDATED=2, SAVED=3; + protected boolean compositeImage; + protected int width; + protected int height; + protected boolean locked; + private int lockedCount; + private Thread lockingThread; + protected int nChannels = 1; + protected int nSlices = 1; + protected int nFrames = 1; + protected boolean dimensionsSet; + + private ImageJ ij = IJ.getInstance(); + private String title; + private String url; + private FileInfo fileInfo; + private int imageType = GRAY8; + private boolean typeSet; + private ImageStack stack; + private static int currentID = -1; + private int ID; + private static Component comp; + private boolean imageLoaded; + private int imageUpdateY, imageUpdateW; + private Properties properties; + private long startTime; + private Calibration calibration; + private static Calibration globalCalibration; + private boolean activated; + private boolean ignoreFlush; + private boolean errorLoadingImage; + private static ImagePlus clipboard; + private static Vector listeners = new Vector(); + private boolean openAsHyperStack; + private int[] position = {1,1,1}; + private boolean noUpdateMode; + private ImageCanvas flatteningCanvas; + private Overlay overlay; + private boolean compositeChanges; + private boolean hideOverlay; + private static int default16bitDisplayRange; + private boolean antialiasRendering = true; + private boolean ignoreGlobalCalibration; + private boolean oneSliceStack; + public boolean setIJMenuBar = Prefs.setIJMenuBar; + private Plot plot; + private Properties imageProperties; + private Color borderColor; + private boolean temporary; + + + /** Constructs an uninitialized ImagePlus. */ + public ImagePlus() { + title = (this instanceof CompositeImage)?"composite":"null"; + setID(); + } + + /** Constructs an ImagePlus from an Image or BufferedImage. The first + argument will be used as the title of the window that displays the image. + Throws an IllegalStateException if an error occurs while loading the image. */ + public ImagePlus(String title, Image image) { + this.title = title; + if (image!=null) + setImage(image); + setID(); + } + + /** Constructs an ImagePlus from an ImageProcessor. */ + public ImagePlus(String title, ImageProcessor ip) { + setProcessor(title, ip); + setID(); + } + + /** Constructs an ImagePlus from a TIFF, BMP, DICOM, FITS, + PGM, GIF or JPRG specified by a path or from a TIFF, DICOM, + GIF or JPEG specified by a URL. */ + public ImagePlus(String pathOrURL) { + Opener opener = new Opener(); + ImagePlus imp = null; + boolean isURL = pathOrURL.indexOf("://")>0; + if (isURL) + imp = opener.openURL(pathOrURL); + else + imp = opener.openImage(pathOrURL); + if (imp!=null) { + if (imp.getStackSize()>1) + setStack(imp.getTitle(), imp.getStack()); + else + setProcessor(imp.getTitle(), imp.getProcessor()); + setCalibration(imp.getCalibration()); + properties = imp.getProperties(); + setFileInfo(imp.getOriginalFileInfo()); + setDimensions(imp.getNChannels(), imp.getNSlices(), imp.getNFrames()); + setOverlay(imp.getOverlay()); + setRoi(imp.getRoi()); + if (isURL) + this.url = pathOrURL; + setID(); + } + } + + /** Constructs an ImagePlus from a stack. */ + public ImagePlus(String title, ImageStack stack) { + setStack(title, stack); + setID(); + } + + private void setID() { + ID = --currentID; + } + + public void setTemporary() { + if (!temporary) { + temporary = true; + currentID++; + ID = -Integer.MAX_VALUE; + } + } + + /** Locks the image so other threads can test to see if it is in use. + * One thread can lock an image multiple times, then it has to unlock + * it as many times until it is unlocked. This allows nested locking + * within a thread. + * Returns true if the image was successfully locked. + * Beeps, displays a message in the status bar, and returns + * false if the image is already locked by another thread. + */ + public synchronized boolean lock() { + return lock(true); + } + + /** Similar to lock, but doesn't beep and display an error + * message if the attempt to lock the image fails. + */ + public synchronized boolean lockSilently() { + return lock(false); + } + + private synchronized boolean lock(boolean loud) { + if (locked) { + if (Thread.currentThread()==lockingThread) { + lockedCount++; //allow locking multiple times by the same thread + return true; + } else { + if (loud) { + IJ.beep(); + IJ.showStatus("\"" + title + "\" is locked"); + if (IJ.debugMode) IJ.log(title + " is locked by " + lockingThread + "; refused locking by " + Thread.currentThread().getName()); + if (IJ.macroRunning()) + IJ.wait(500); + } + return false; + } + } else { + locked = true; //we could use 'lockedCount instead, but subclasses might use + lockedCount = 1; + lockingThread = Thread.currentThread(); + if (win instanceof StackWindow) + ((StackWindow)win).setSlidersEnabled(false); + if (IJ.debugMode) IJ.log(title + ": locked" + (loud ? "" : "silently") + " by " + Thread.currentThread().getName()); + return true; + } + } + + /** Unlocks the image. + * In case the image had been locked several times by the current thread, + * it gets unlocked only after as many unlock operations as there were + * previous lock operations. + */ + public synchronized void unlock() { + if (Thread.currentThread()==lockingThread && lockedCount>1) + lockedCount--; + else { + locked = false; + lockedCount = 0; + lockingThread = null; + if (win instanceof StackWindow) + ((StackWindow)win).setSlidersEnabled(true); + if (IJ.debugMode) IJ.log(title + ": unlocked"); + } + } + + /** Returns 'true' if the image is locked. */ + public boolean isLocked() { + return locked; + } + + /** Returns 'true' if the image was locked on another thread. */ + public boolean isLockedByAnotherThread() { + return locked && Thread.currentThread()!=lockingThread; + } + + private void waitForImage(Image image) { + if (comp==null) { + comp = IJ.getInstance(); + if (comp==null) + comp = new Canvas(); + } + imageLoaded = false; + if (!comp.prepareImage(image, this)) { + double progress; + waitStart = System.currentTimeMillis(); + while (!imageLoaded && !errorLoadingImage) { + IJ.wait(30); + if (imageUpdateW>1) { + progress = (double)imageUpdateY/imageUpdateW; + if (!(progress<1.0)) { + progress = 1.0 - (progress-1.0); + if (progress<0.0) progress = 0.9; + } + showProgress(progress); + } + } + showProgress(1.0); + } + } + + long waitStart; + private void showProgress(double percent) { + if ((System.currentTimeMillis()-waitStart)>500L) + IJ.showProgress(percent); + } + + /** Draws the image. If there is an ROI, its + outline is also displayed. Does nothing if there + is no window associated with this image (i.e. show() + has not been called).*/ + public void draw() { + if (win!=null) + win.getCanvas().repaint(); + } + + /** Draws image and roi outline using a clip rect. */ + public void draw(int x, int y, int width, int height){ + if (win!=null) { + ImageCanvas ic = win.getCanvas(); + double mag = ic.getMagnification(); + x = ic.screenX(x); + y = ic.screenY(y); + width = (int)(width*mag); + height = (int)(height*mag); + ic.repaint(x, y, width, height); + if (listeners.size()>0 && roi!=null && roi.getPasteMode()!=Roi.NOT_PASTING) + notifyListeners(UPDATED); + } + } + + /** Updates this image from the pixel data in its + associated ImageProcessor, then displays it. Does + nothing if there is no window associated with + this image (i.e. show() has not been called).*/ + public synchronized void updateAndDraw() { + if (win==null) { + img = null; + return; + } + if (stack!=null && !stack.isVirtual() && currentSlice>=1 && currentSlice<=stack.size()) { + if (stack.size()>1 && win!=null && !(win instanceof StackWindow)) { + setStack(stack); //adds scroll bar if stack size has changed to >1 + return; + } + Object pixels = stack.getPixels(currentSlice); + if (ip!=null && pixels!=null && pixels!=ip.getPixels()) { // was stack updated? + try { + ip.setPixels(pixels); + ip.setSnapshotPixels(null); + } catch(Exception e) {} + } + } + if (win!=null) { + win.getCanvas().setImageUpdated(); + if (listeners.size()>0) notifyListeners(UPDATED); + } + draw(); + } + + /** Use to update the image when the underlying virtual stack changes. */ + public void updateVirtualSlice() { + ImageStack vstack = getStack(); + if (vstack.isVirtual()) { + double min=getDisplayRangeMin(), max=getDisplayRangeMax(); + setProcessor(vstack.getProcessor(getCurrentSlice())); + setDisplayRange(min,max); + } else + throw new IllegalArgumentException("Virtual stack required"); + } + + /** Sets the display mode of composite color images, where 'mode' + should be IJ.COMPOSITE, IJ.COLOR or IJ.GRAYSCALE. */ + public void setDisplayMode(int mode) { + if (this instanceof CompositeImage) { + ((CompositeImage)this).setMode(mode); + updateAndDraw(); + } + } + + /** Returns the display mode (IJ.COMPOSITE, IJ.COLOR + or IJ.GRAYSCALE) if this is a composite color + image, or 0 if it not. */ + public int getDisplayMode() { + if (this instanceof CompositeImage) + return ((CompositeImage)this).getMode(); + else + return 0; + } + + /** Controls which channels in a composite color image are displayed, + where 'channels' is a list of ones and zeros that specify the channels to + display. For example, "101" causes channels 1 and 3 to be displayed. */ + public void setActiveChannels(String channels) { + if (!(this instanceof CompositeImage)) + return; + boolean[] active = ((CompositeImage)this).getActiveChannels(); + for (int i=0; ii && channels.charAt(i)=='1') + b = true; + active[i] = b; + } + updateAndDraw(); + Channels.updateChannels(); + } + + /** Updates this image from the pixel data in its + associated ImageProcessor, then displays it. + The CompositeImage class overrides this method + to only update the current channel. */ + public void updateChannelAndDraw() { + updateAndDraw(); + } + + /** Returns a reference to the current ImageProcessor. The + CompositeImage class overrides this method to return + the processor associated with the current channel. */ + public ImageProcessor getChannelProcessor() { + return getProcessor(); + } + + /** Returns an array containing the lookup tables used by this image, + * one per channel, or an empty array if this is an RGB image. + * @see #getNChannels + * @see #isComposite + * @see #getCompositeMode + */ + public LUT[] getLuts() { + ImageProcessor ip2 = getProcessor(); + if (ip2==null) + return new LUT[0]; + LUT lut = ip2.getLut(); + if (lut==null) + return new LUT[0]; + LUT[] luts = new LUT[1]; + luts[0] = lut; + return luts; + } + + /** Calls draw to draw the image and also repaints the + image window to force the information displayed above + the image (dimension, type, size) to be updated. */ + public void repaintWindow() { + if (win!=null) { + draw(); + win.repaint(); + } + } + + /** Calls updateAndDraw to update from the pixel data + and draw the image, and also repaints the image + window to force the information displayed above + the image (dimension, type, size) to be updated. */ + public void updateAndRepaintWindow() { + if (win!=null) { + updateAndDraw(); + win.repaint(); + } + } + + /** ImageCanvas.paint() calls this method when the + ImageProcessor has generated a new image. */ + public void updateImage() { + if (win==null) { + img = null; + return; + } + if (ip!=null) + img = ip.createImage(); + } + + /** Closes the window, if any, that is displaying this image. */ + public void hide() { + if (win==null) { + img = null; + Interpreter.removeBatchModeImage(this); + return; + } + boolean unlocked = lockSilently(); + Overlay overlay2 = getOverlay(); + changes = false; + win.close(); + win = null; + setOverlay(overlay2); + if (unlocked) unlock(); + } + + /** Closes this image and sets the ImageProcessor to null. To avoid the + "Save changes?" dialog, first set the public 'changes' variable to false. */ + public void close() { + ImageWindow win = getWindow(); + if (win!=null) + win.close(); + else { + if (WindowManager.getCurrentImage()==this) + WindowManager.setTempCurrentImage(null); + deleteRoi(); //save any ROI so it can be restored later + Interpreter.removeBatchModeImage(this); + } + } + + /** Opens a window to display this image and clears the status bar. */ + public void show() { + show(""); + } + + /** Opens a window to display this image and displays + 'statusMessage' in the status bar. */ + public void show(String statusMessage) { + if (isVisible() || temporary) + return; + win = null; + if ((IJ.isMacro() && ij==null) || Interpreter.isBatchMode()) { + if (isComposite()) ((CompositeImage)this).reset(); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) imp.saveRoi(); + WindowManager.setTempCurrentImage(this); + Interpreter.addBatchModeImage(this); + return; + } + if (Prefs.useInvertingLut && getBitDepth()==8 && ip!=null && !ip.isInvertedLut()&& !ip.isColorLut()) + invertLookupTable(); + img = getImage(); + if ((img!=null) && (width>=0) && (height>=0)) { + activated = false; + int stackSize = getStackSize(); + if (stackSize>1) + win = new StackWindow(this); // displays the window and (if macro) waits for window to be activated + else if (getProperty(Plot.PROPERTY_KEY) != null) + win = new PlotWindow(this, (Plot)(getProperty(Plot.PROPERTY_KEY))); + else + win = new ImageWindow(this); + if (roi!=null) roi.setImage(this); + if (overlay!=null && getCanvas()!=null) + getCanvas().setOverlay(overlay); + IJ.showStatus(statusMessage); + if (IJ.isMacro() && stackSize==1) // for non-stacks, wait for window to be activated + waitTillActivated(); + if (imageType==GRAY16 && default16bitDisplayRange!=0) { + resetDisplayRange(); + updateAndDraw(); + } + if (stackSize>1) { + int c = getChannel(); + int z = getSlice(); + int t = getFrame(); + if (c>1 || z>1 || t>1) + setPosition(c, z, t); + } + if (setIJMenuBar) + IJ.wait(25); + notifyListeners(OPENED); + } + } + + void invertLookupTable() { + int nImages = getStackSize(); + ip.invertLut(); + if (nImages==1) + ip.invert(); + else { + ImageStack stack2 = getStack(); + for (int i=1; i<=nImages; i++) + stack2.getProcessor(i).invert(); + stack2.setColorModel(ip.getColorModel()); + } + } + + /** Waits until the image window becomes activated. This is necessary in + * macros or other programs if an ImagePlus is shown on the screen, + * because displaying the window is asynchronous (happens later) + * and will make the image the active one. Without waiting, in the + * meanwhile another window could be already the active one and would + * become deactivated. + * If the ImagePlus may have been displayed previously, first call + * setDeactivated(). + * ImagePlus.show() and new StackWindow(ImagePlus) + * call this method if IJ.isMacro() is true, i.e., when running a macro or + * executing an IJ.run(...) call. + */ + public void waitTillActivated() { + if (win == null) return; + if (EventQueue.isDispatchThread()) { //'activated' is set in the EventQueue, we can't wait for it in the EventQueue + WindowManager.setTempCurrentImage(this); + return; + } + long start = System.currentTimeMillis(); + while (!activated) { + IJ.wait(5); + if (ij != null && ij.quitting()) return; + if ((System.currentTimeMillis()-start)>2000) { + WindowManager.setTempCurrentImage(this); + break; // 2 second timeout + } + } + } + + /** Called by ImageWindow.windowActivated(); to end waiting in waitTillActivated. */ + public void setActivated() { + activated = true; + if (borderColor!=null && win!=null) + win.setBackground(borderColor); + } + + /** Called by new StackWindow(ImagePlus) + * before showing the StackWindow, to prepare for + * waitTillActivated(). + */ + public void setDeactivated() { + activated = false; + } + + /** Returns this image as a AWT image. */ + public Image getImage() { + if (img==null && ip!=null) + img = ip.createImage(); + return img; + } + + /** Returns a copy of this image as an 8-bit or RGB BufferedImage. + * @see ij.process.ShortProcessor#get16BitBufferedImage + */ + public BufferedImage getBufferedImage() { + if (isComposite()) + return (new ColorProcessor(getImage())).getBufferedImage(); + else + return ip.getBufferedImage(); + } + + /** Returns this image's unique numeric ID. */ + public int getID() { + return ID; + } + + /** Replaces the image, if any, with the one specified. + * Throws an IllegalStateException if an error occurs + * while loading the image. + */ + public void setImage(Image image) { + if (image instanceof BufferedImage) { + BufferedImage bi = (BufferedImage)image; + int nBands = bi.getSampleModel().getNumBands(); + int type = bi.getType(); + boolean rgb = type==BufferedImage.TYPE_3BYTE_BGR || type==BufferedImage.TYPE_INT_RGB || type==BufferedImage.TYPE_4BYTE_ABGR; + if (nBands>1 && !rgb) { + ImageStack biStack = new ImageStack(bi.getWidth(), bi.getHeight()); + for (int b=0; b0?GRAY8:COLOR_RGB; + if (image!=null && type==COLOR_RGB) + ip = new ColorProcessor(image); + if (ip==null && image!=null) + ip = new ByteProcessor(image); + setType(type); + this.img = ip.createImage(); + if (win!=null) { + if (dimensionsChanged) + win = new ImageWindow(this); + else + repaintWindow(); + } + } + + /** + * Extract pixels as an an ImageProcessor from a single band of a BufferedImage. + * @param img + * @param band + * @return + */ + public static ImageProcessor convertToImageProcessor(BufferedImage img, int band) { + int w = img.getWidth(); + int h = img.getHeight(); + int dataType = img.getSampleModel().getDataType(); + // Read data as float (no matter what it is - it's the most accuracy ImageJ can provide) + FloatProcessor fp = new FloatProcessor(w, h); + float[] pixels = (float[])fp.getPixels(); + img.getRaster().getSamples(0, 0, w, h, band, pixels); + // Convert to 8 or 16-bit, if appropriate + if (dataType == DataBuffer.TYPE_BYTE) { + ByteProcessor bp = new ByteProcessor(w, h); + bp.setPixels(0, fp); + return bp; + } else if (dataType == DataBuffer.TYPE_USHORT) { + ShortProcessor sp = new ShortProcessor(w, h); + sp.setPixels(0, fp); + return sp; + } else + return fp; + } + + /** Replaces this image with the specified ImagePlus. May + not work as expected if 'imp' is a CompositeImage + and this image is not. */ + public void setImage(ImagePlus imp) { + Properties newProperties = imp.getProperties(); + if (newProperties!=null) + newProperties = (Properties)(newProperties.clone()); + if (imp.getWindow()!=null) + imp = imp.duplicate(); + ImageStack stack2 = imp.getStack(); + if (imp.isHyperStack()) + setOpenAsHyperStack(true); + LUT[] luts = null; + if (imp.isComposite() && (this instanceof CompositeImage)) { + if (((CompositeImage)imp).getMode()!=((CompositeImage)this).getMode()) + ((CompositeImage)this).setMode(((CompositeImage)imp).getMode()); + luts = ((CompositeImage)imp).getLuts(); + } + LUT lut = !imp.isComposite()?imp.getProcessor().getLut():null; + setStack(stack2, imp.getNChannels(), imp.getNSlices(), imp.getNFrames()); + compositeImage = imp.isComposite(); + if (luts!=null) { + ((CompositeImage)this).setLuts(luts); + ((CompositeImage)this).setMode(((CompositeImage)imp).getMode()); + updateAndRepaintWindow(); + } else if (lut!=null) { + getProcessor().setLut(lut); + updateAndRepaintWindow(); + } + setTitle(imp.getTitle()); + setCalibration(imp.getCalibration()); + setOverlay(imp.getOverlay()); + properties = newProperties; + if (getProperty(Plot.PROPERTY_KEY)!=null && win instanceof PlotWindow) { + Plot plot = (Plot)(getProperty(Plot.PROPERTY_KEY)); + ((PlotWindow)win).setPlot(plot); + plot.setImagePlus(this); + } + setFileInfo(imp.getOriginalFileInfo()); + setProperty ("Info", imp.getProperty ("Info")); + setProperties(imp.getPropertiesAsArray()); + } + + /** Replaces the ImageProcessor with the one specified and updates the + display. With stacks, the ImageProcessor must be the same type as the + other images in the stack and it must be the same width and height. */ + public void setProcessor(ImageProcessor ip) { + setProcessor(null, ip); + } + + /** Replaces the ImageProcessor with the one specified and updates the display. With + stacks, the ImageProcessor must be the same type as other images in the stack and + it must be the same width and height. Set 'title' to null to leave the title unchanged. */ + public void setProcessor(String title, ImageProcessor ip) { + if (ip==null || ip.getPixels()==null) + throw new IllegalArgumentException("ip null or ip.getPixels() null"); + if (getStackSize()>1) { + if (ip.getWidth()!=width || ip.getHeight()!=height) + throw new IllegalArgumentException("Wrong dimensions for this stack"); + int stackBitDepth = stack!=null?stack.getBitDepth():0; + if (stackBitDepth>0 && getBitDepth()!=stackBitDepth) + throw new IllegalArgumentException("Wrong type for this stack"); + } else { + setStackNull(); + setCurrentSlice(1); + } + setProcessor2(title, ip, null); + } + + void setProcessor2(String title, ImageProcessor ip, ImageStack newStack) { + if (title!=null) setTitle(title); + if (ip==null) + return; + this.ip = ip; + if (this.ip!=null && getWindow()!=null) + notifyListeners(UPDATED); + if (ij!=null) + ip.setProgressBar(ij.getProgressBar()); + int stackSize = 1; + boolean dimensionsChanged = width>0 && height>0 && (width!=ip.getWidth() || height!=ip.getHeight()); + if (stack!=null) { + stackSize = stack.size(); + if (currentSlice>stackSize) + setCurrentSlice(stackSize); + if (currentSlice>=1 && currentSlice<=stackSize && !dimensionsChanged) + stack.setPixels(ip.getPixels(),currentSlice); + } + img = null; + if (dimensionsChanged) roi = null; + int type; + if (ip instanceof ByteProcessor) + type = GRAY8; + else if (ip instanceof ColorProcessor) + type = COLOR_RGB; + else if (ip instanceof ShortProcessor) + type = GRAY16; + else + type = GRAY32; + if (width==0) + imageType = type; + else + setType(type); + width = ip.getWidth(); + height = ip.getHeight(); + if (win!=null) { + if (dimensionsChanged && stackSize==1) + win.updateImage(this); + else if (newStack==null) + repaintWindow(); + draw(); + } + } + + /** Replaces the image with the specified stack and updates the display. */ + public void setStack(ImageStack stack) { + setStack(null, stack); + } + + /** Replaces the image with the specified stack and updates + the display. Set 'title' to null to leave the title unchanged. */ + public void setStack(String title, ImageStack newStack) { + int bitDepth1 = getBitDepth(); + int previousStackSize = getStackSize(); + int newStackSize = newStack.getSize(); + if (newStackSize==0) + throw new IllegalArgumentException("Stack is empty"); + if (!newStack.isVirtual()) { + Object[] arrays = newStack.getImageArray(); + if (arrays==null || (arrays.length>0&&arrays[0]==null)) + throw new IllegalArgumentException("Stack pixel array null"); + } + boolean sliderChange = false; + if (win!=null && (win instanceof StackWindow)) { + int nScrollbars = ((StackWindow)win).getNScrollbars(); + if (nScrollbars>0 && newStackSize==1) + sliderChange = true; + else if (nScrollbars==0 && newStackSize>1) + sliderChange = true; + } + if (currentSlice<1) setCurrentSlice(1); + boolean resetCurrentSlice = currentSlice>newStackSize; + if (resetCurrentSlice) setCurrentSlice(newStackSize); + ImageProcessor ip = newStack.getProcessor(currentSlice); + boolean dimensionsChanged = width>0 && height>0 && (width!=ip.getWidth()||height!=ip.getHeight()); + if (this.stack==null) + newStack.viewers(+1); + this.stack = newStack; + oneSliceStack = false; + setProcessor2(title, ip, newStack); + if (bitDepth1!=0 && bitDepth1!=getBitDepth()) + compositeChanges = true; + if (compositeChanges && (this instanceof CompositeImage)) { + this.compositeImage = getStackSize()!=getNSlices(); + ((CompositeImage)this).completeReset(); + if (bitDepth1!=0 && bitDepth1!=getBitDepth()) + ((CompositeImage)this).resetDisplayRanges(); + } + compositeChanges = false; + if (win==null) { + if (resetCurrentSlice) setSlice(currentSlice); + return; + } + boolean invalidDimensions = (isDisplayedHyperStack()||(this instanceof CompositeImage)) && (win instanceof StackWindow) && !((StackWindow)win).validDimensions(); + if (newStackSize>1 && !(win instanceof StackWindow)) { + if (isDisplayedHyperStack()) + setOpenAsHyperStack(true); + activated = false; + win = new StackWindow(this, dimensionsChanged?null:getCanvas()); // replaces this window + if (IJ.isMacro()) waitTillActivated(); // wait for stack window to be activated + setPosition(1, 1, 1); + } else if (newStackSize>1 && invalidDimensions) { + if (isDisplayedHyperStack()) + setOpenAsHyperStack(true); + win = new StackWindow(this); // replaces this window + setPosition(1, 1, 1); + } else if (dimensionsChanged || sliderChange) { + win.updateImage(this); + } else { + if (win!=null && win instanceof StackWindow) + ((StackWindow)win).updateSliceSelector(); + if (isComposite()) { + ((CompositeImage)this).reset(); + updateAndDraw(); + } + repaintWindow(); + } + if (resetCurrentSlice) + setSlice(currentSlice); + } + + public void setStack(ImageStack newStack, int channels, int slices, int frames) { + if (newStack==null || channels*slices*frames!=newStack.getSize()) + throw new IllegalArgumentException("channels*slices*frames!=stackSize"); + if (IJ.debugMode) IJ.log("setStack: "+newStack.getSize()+" "+channels+" "+slices+" "+frames+" "+isComposite()); + compositeChanges = channels!=this.nChannels; + this.nChannels = channels; + this.nSlices = slices; + this.nFrames = frames; + setStack(null, newStack); + } + + private synchronized void setStackNull() { + if (oneSliceStack && stack!=null && stack.size()>0) { + String label = stack.getSliceLabel(1); + setProp("Slice_Label", label); + } + stack = null; + oneSliceStack = false; + } + + /** Saves this image's FileInfo so it can be later + retieved using getOriginalFileInfo(). */ + public void setFileInfo(FileInfo fi) { + if (fi!=null) { + fi.pixels = null; + if (fi.imageSaved) { + notifyListeners(SAVED); + fi.imageSaved = false; + } + } + fileInfo = fi; + } + + /** Returns the ImageWindow that is being used to display + this image. Returns null if show() has not be called + or the ImageWindow has been closed. */ + public ImageWindow getWindow() { + return win; + } + + /** Returns true if this image is currently being displayed in a window. */ + public boolean isVisible() { + return win!=null && win.isVisible(); + } + + /** This method should only be called from an ImageWindow. */ + public void setWindow(ImageWindow win) { + this.win = win; + if (roi!=null) + roi.setImage(this); // update roi's 'ic' field + } + + /** Returns the ImageCanvas being used to + display this image, or null. */ + public ImageCanvas getCanvas() { + return win!=null?win.getCanvas():flatteningCanvas; + } + + /** Sets current foreground color. */ + public void setColor(Color c) { + if (ip!=null) + ip.setColor(c); + } + + void setupProcessor() { + } + + public boolean isProcessor() { + return ip!=null; + } + + /** Returns a reference to the current ImageProcessor. If there + is no ImageProcessor, it creates one. Returns null if this + ImagePlus contains no ImageProcessor and no AWT Image. + Sets the line width to the current line width and sets the + calibration table if the image is density calibrated. */ + public ImageProcessor getProcessor() { + if (ip==null) + return null; + if (roi!=null && roi.isArea()) + ip.setRoi(roi.getBounds()); + else + ip.resetRoi(); + if (!compositeImage) + ip.setLineWidth(Line.getWidth()); + if (ij!=null) + ip.setProgressBar(ij.getProgressBar()); + Calibration cal = getCalibration(); + if (cal.calibrated()) + ip.setCalibrationTable(cal.getCTable()); + else + ip.setCalibrationTable(null); + if (Recorder.record) { + Recorder recorder = Recorder.getInstance(); + if (recorder!=null) recorder.imageUpdated(this); + } + return ip; + } + + /** Frees RAM by setting the snapshot (undo) buffer in + the current ImageProcessor to null. */ + public void trimProcessor() { + ImageProcessor ip2 = ip; + if (!locked && ip2!=null) { + if (IJ.debugMode) IJ.log(title + ": trimProcessor"); + Roi roi2 = getRoi(); + if (roi2!=null && roi2.getPasteMode()!=Roi.NOT_PASTING) + roi2.endPaste(); + ip2.setSnapshotPixels(null); + } + } + + /** For images with irregular ROIs, returns a byte mask, otherwise, returns + * null. Mask pixels have a non-zero value.and the dimensions of the + * mask are equal to the width and height of the ROI. + * @see ij.ImagePlus#createRoiMask + * @see ij.ImagePlus#createThresholdMask + */ + public ImageProcessor getMask() { + if (roi==null) { + if (ip!=null) ip.resetRoi(); + return null; + } + ImageProcessor mask = roi.getMask(); + if (mask==null) + return null; + if (ip!=null && roi!=null) { + ip.setMask(mask); + ip.setRoi(roi.getBounds()); + } + return mask; + } + + /** Returns an 8-bit binary (foreground=255, background=0) + * ROI or overlay mask that has the same dimensions + * as this image. Creates an ROI mask If the image has both + * both an ROI and an overlay. Set the threshold of the mask to 255. + * @see #createThresholdMask + * @see ij.gui.Roi#getMask + */ + public ByteProcessor createRoiMask() { + Roi roi2 = getRoi(); + Overlay overlay2 = getOverlay(); + if (roi2==null && overlay2==null) + throw new IllegalArgumentException("ROI or overlay required"); + ByteProcessor mask = new ByteProcessor(getWidth(),getHeight()); + mask.setColor(255); + if (roi2!=null) + mask.fill(roi2); + else if (overlay2!=null) { + if (overlay2.size()==1 && (overlay2.get(0) instanceof ImageRoi)) { + ImageRoi iRoi = (ImageRoi)overlay2.get(0); + ImageProcessor ip = iRoi.getProcessor(); + if (ip.getWidth()!=mask.getWidth() || ip.getHeight()!=mask.getHeight()) + return mask; + for (int i=0; i + imp = IJ.getImage(); + stats = imp.getStatistics(); + IJ.log("Area: "+stats.area); + IJ.log("Mean: "+stats.mean); + IJ.log("Max: "+stats.max); + + @return an {@link ij.process.ImageStatistics} object + @see #getAllStatistics + @see #getRawStatistics + @see ij.process.ImageProcessor#getStats + */ + public ImageStatistics getStatistics() { + return getStatistics(AREA+MEAN+STD_DEV+MODE+MIN_MAX+RECT); + } + + /** This method returns complete calibrated statistics for this + * image or ROI (with "Limit to threshold"), but it is up to 70 times + * slower than getStatistics(). + * @return an {@link ij.process.ImageStatistics} object + * @see #getStatistics + * @see ij.process.ImageProcessor#getStatistics + */ + public ImageStatistics getAllStatistics() { + return getStatistics(ALL_STATS+LIMIT); + } + + /* Returns uncalibrated statistics for this image or ROI, including + 256 bin histogram, pixelCount, mean, mode, min and max. */ + public ImageStatistics getRawStatistics() { + if (roi!=null && roi.isArea()) + ip.setRoi(roi); + else + ip.resetRoi(); + return ImageStatistics.getStatistics(ip, AREA+MEAN+MODE+MIN_MAX, null); + } + + /** Returns an ImageStatistics object generated using the + specified measurement options. + @see ij.measure.Measurements + */ + public ImageStatistics getStatistics(int mOptions) { + return getStatistics(mOptions, 256, 0.0, 0.0); + } + + /** Returns an ImageStatistics object generated using the + specified measurement options and histogram bin count. */ + public ImageStatistics getStatistics(int mOptions, int nBins) { + return getStatistics(mOptions, nBins, 0.0, 0.0); + } + + /** Returns an ImageStatistics object generated using the + specified measurement options, histogram bin count + and histogram range. */ + public ImageStatistics getStatistics(int mOptions, int nBins, double histMin, double histMax) { + ImageProcessor ip2 = ip; + int bitDepth = getBitDepth(); + if (nBins!=256 && (bitDepth==8||bitDepth==24)) + ip2 =ip.convertToShort(false); + Roi roi2 = roi; + if (roi2==null) + ip2.resetRoi(); + else if (roi2.isArea()) + ip2.setRoi(roi2); + else if ((roi2 instanceof PointRoi) && roi2.size()==1) { + // needed to be consistent with ImageProcessor.getStatistics() + FloatPolygon p = roi2.getFloatPolygon(); + ip2.setRoi((int)p.xpoints[0], (int)p.ypoints[0], 1, 1); + } + ip2.setHistogramSize(nBins); + Calibration cal = getCalibration(); + if (getType()==GRAY16&& !(histMin==0.0&&histMax==0.0)) { + histMin = cal.getRawValue(histMin); + histMax=cal.getRawValue(histMax); + } + ip2.setHistogramRange(histMin, histMax); + ImageStatistics stats = ImageStatistics.getStatistics(ip2, mOptions, cal); + ip2.setHistogramSize(256); + ip2.setHistogramRange(0.0, 0.0); + return stats; + } + + /** Returns the image name. */ + public String getTitle() { + if (title==null) + return ""; + else + return title; + } + + /** If the image title is a file name, returns the name + without the extension and with spaces removed, + otherwise returns the title shortened to the first space. + */ + public String getShortTitle() { + String title = getTitle().trim(); + int index = title.lastIndexOf('.'); + boolean fileName = index>0; + if (fileName) { + title = title.substring(0, index); + title = title.replaceAll(" ",""); + } else { + index = title.indexOf(' '); + if (index>-1 && !fileName) + title = title.substring(0, index); + } + return title; + } + + /** Sets the image name. */ + public void setTitle(String title) { + if (title==null) + return; + if (win!=null) { + if (ij!=null) + Menus.updateWindowMenuItem(this, this.title, title); + String virtual = stack!=null && stack.isVirtual()?" (V)":""; + String global = getGlobalCalibration()!=null?" (G)":""; + String scale = ""; + double magnification = win.getCanvas().getMagnification(); + if (magnification!=1.0) { + double percent = magnification*100.0; + int digits = percent>100.0||percent==(int)percent?0:1; + scale = " (" + IJ.d2s(percent,digits) + "%)"; + } + win.setTitle(title+virtual+global+scale); + } + boolean titleChanged = !title.equals(this.title); + this.title = title; + if (titleChanged && listeners.size()>0) + notifyListeners(UPDATED); + } + + /** Returns the width of this image in pixels. */ + public int getWidth() { + return width; + } + + /** Returns the height of this image in pixels. */ + public int getHeight() { + return height; + } + + /** Returns the size of this image in bytes. */ + public double getSizeInBytes() { + double size = ((double)getWidth()*getHeight()*getStackSize()); + int type = getType(); + switch (type) { + case ImagePlus.GRAY16: size *= 2.0; break; + case ImagePlus.GRAY32: size *= 4.0; break; + case ImagePlus.COLOR_RGB: size *= 4.0; break; + } + return size; + } + + /** If this is a stack, returns the number of slices, else returns 1. */ + public int getStackSize() { + if (stack==null || oneSliceStack) + return 1; + else { + int slices = stack.size(); + if (slices<=0) slices = 1; + return slices; + } + } + + /** If this is a stack, returns the actual number of images in the stack, else returns 1. */ + public int getImageStackSize() { + if (stack==null) + return 1; + else { + int slices = stack.size(); + if (slices==0) slices = 1; + return slices; + } + } + + /** Sets the 3rd, 4th and 5th dimensions, where + nChannels*nSlices*nFrames + must be equal to the stack size. */ + public void setDimensions(int nChannels, int nSlices, int nFrames) { + //IJ.log("setDimensions: "+nChannels+" "+nSlices+" "+nFrames+" "+getImageStackSize()); + if (nChannels*nSlices*nFrames!=getImageStackSize() && ip!=null) { + //throw new IllegalArgumentException("channels*slices*frames!=stackSize"); + nChannels = 1; + nSlices = getImageStackSize(); + nFrames = 1; + if (isDisplayedHyperStack()) { + setOpenAsHyperStack(false); + new StackWindow(this); + setSlice(1); + } + } + boolean updateWin = isDisplayedHyperStack() && (this.nChannels!=nChannels||this.nSlices!=nSlices||this.nFrames!=nFrames); + boolean newSingleImage = win!=null && (win instanceof StackWindow) && nChannels==1&&nSlices==1&&nFrames==1; + if (newSingleImage) updateWin = true; + this.nChannels = nChannels; + this.nSlices = nSlices; + this.nFrames = nFrames; + if (updateWin) { + if (nSlices!=getImageStackSize()) + setOpenAsHyperStack(true); + ip = null; + img = null; + setPositionWithoutUpdate(getChannel(), getSlice(), getFrame()); + if (isComposite()) ((CompositeImage)this).reset(); + new StackWindow(this); + } + dimensionsSet = true; + } + + /** Returns 'true' if this image is a hyperstack. */ + public boolean isHyperStack() { + return isDisplayedHyperStack() || (openAsHyperStack&&getNDimensions()>3); + } + + /** Returns the number of dimensions (2, 3, 4 or 5). */ + public int getNDimensions() { + int dimensions = 2; + int[] dim = getDimensions(true); + if (dim[2]>1) dimensions++; + if (dim[3]>1) dimensions++; + if (dim[4]>1) dimensions++; + return dimensions; + } + + /** Returns 'true' if this is a hyperstack currently being displayed in a StackWindow. */ + public boolean isDisplayedHyperStack() { + return win!=null && win instanceof StackWindow && ((StackWindow)win).isHyperStack(); + } + + /** Returns the number of channels. */ + public int getNChannels() { + verifyDimensions(); + return nChannels; + } + + /** Returns the image depth (number of z-slices). */ + public int getNSlices() { + verifyDimensions(); + return nSlices; + } + + /** Returns the number of frames (time-points). */ + public int getNFrames() { + verifyDimensions(); + return nFrames; + } + + /** Returns the dimensions of this image (width, height, nChannels, + nSlices, nFrames) as a 5 element int array. */ + public int[] getDimensions() { + return getDimensions(true); + } + + public int[] getDimensions(boolean varify) { + if (varify) + verifyDimensions(); + int[] d = new int[5]; + d[0] = width; + d[1] = height; + d[2] = nChannels; + d[3] = nSlices; + d[4] = nFrames; + return d; + } + + void verifyDimensions() { + int stackSize = getImageStackSize(); + if (nSlices==1) { + if (nChannels>1 && nFrames==1) + nChannels = stackSize; + else if (nFrames>1 && nChannels==1) + nFrames = stackSize; + } + if (nChannels*nSlices*nFrames!=stackSize) { + nSlices = stackSize; + nChannels = 1; + nFrames = 1; + } + } + + /** Returns the current image type (ImagePlus.GRAY8, ImagePlus.GRAY16, + ImagePlus.GRAY32, ImagePlus.COLOR_256 or ImagePlus.COLOR_RGB). + @see #getBitDepth + */ + public int getType() { + return imageType; + } + + /** Returns the bit depth, 8, 16, 24 (RGB) or 32, or 0 if the bit depth + is unknown. RGB images actually use 32 bits per pixel. */ + public int getBitDepth() { + ImageProcessor ip2 = ip; + if (ip2==null) { + int bitDepth = 0; + switch (imageType) { + case GRAY8: bitDepth=typeSet?8:0; break; + case COLOR_256: bitDepth=8; break; + case GRAY16: bitDepth=16; break; + case GRAY32: bitDepth=32; break; + case COLOR_RGB: bitDepth=24; break; + } + return bitDepth; + } + if (ip2 instanceof ByteProcessor) + return 8; + else if (ip2 instanceof ShortProcessor) + return 16; + else if (ip2 instanceof ColorProcessor) + return 24; + else if (ip2 instanceof FloatProcessor) + return 32; + return 0; + } + + /** Returns the number of bytes per pixel. */ + public int getBytesPerPixel() { + switch (imageType) { + case GRAY16: return 2; + case GRAY32: case COLOR_RGB: return 4; + default: return 1; + } + } + + protected void setType(int type) { + if ((type<0) || (type>COLOR_RGB)) + return; + int previousType = imageType; + imageType = type; + if (imageType!=previousType) { + if (win!=null) + Menus.updateMenus(); + getLocalCalibration().setImage(this); + } + typeSet = true; + } + + public void setTypeToColor256() { + if (imageType==ImagePlus.GRAY8) { + ImageProcessor ip2 = getProcessor(); + if (ip2!=null && ip2.getMinThreshold()==ImageProcessor.NO_THRESHOLD && ip2.isColorLut() && !ip2.isPseudoColorLut()) { + imageType = COLOR_256; + typeSet = true; + } + } + } + + + /** Returns the string value from the "Info" property string + * associated with 'key', or null if the key is not found. + * Works with DICOM tags and Bio-Formats metadata. + * @see #getNumericProperty + * @see #getInfoProperty + * @see #getProp + * @see #setProp + */ + public String getStringProperty(String key) { + if (key==null) + return null; + if (isDicomTag(key)) + return DicomTools.getTag(this, key); + if (getStackSize()>1) { + ImageStack stack2 = getStack(); + String label = stack2.getSliceLabel(getCurrentSlice()); + if (label!=null && label.indexOf('\n')>0) { + String value = getStringProperty(key, label); + if (value!=null) + return value; + } + } + Object obj = getProperty("Info"); + if (obj==null || !(obj instanceof String)) + return null; + String info = (String)obj; + return getStringProperty(key, info); + } + + private boolean isDicomTag(String key) { + if (key.length()!=9 || key.charAt(4)!=',') + return false; + key = key.toLowerCase(); + for (int i=0; i<9; i++) { + char c = i!=4?key.charAt(i):'0'; + if (!(Character.isDigit(c)||(c=='a'||c=='b'||c=='c'||c=='d'||c=='e'||c=='f'))) + return false; + } + return true; + } + + /** Returns the numeric value from the "Info" property string + * associated with 'key', or NaN if the key is not found or the + * value associated with the key is not numeric. Works with + * DICOM tags and Bio-Formats metadata. + * @see #getStringProperty + * @see #getInfoProperty + */ + public double getNumericProperty(String key) { + return Tools.parseDouble(getStringProperty(key)); + } + + private String getStringProperty(String key, String info) { + int index1 = -1; + index1 = findKey(info, key+": "); // standard 'key: value' pair? + if (index1<0) // Bio-Formats metadata? + index1 = findKey(info, key+" = "); + if (index1<0) // '=' with no spaces + index1 = findKey(info, key+"="); + if (index1<0) // otherwise not found + return null; + if (index1==info.length()) + return ""; //empty value at the end + int index2 = info.indexOf("\n", index1); + if (index2==-1) + index2=info.length(); + String value = info.substring(index1, index2); + return value; + } + + /** Find a key in a String (words merely ending with 'key' don't qualify). + * @return index of first character after the key, or -1 if not found + */ + private int findKey(String s, String key) { + int i = s.indexOf(key); + if (i<0) + return -1; //key not found + while (i>0 && Character.isLetterOrDigit(s.charAt(i-1))) + i = s.indexOf(key, i+key.length()); + if (i>=0) + return i + key.length(); + else + return -1; + } + + /** Adds a key-value pair to this image's string properties. + * The key-value pair is removed if 'value' is null. The + * properties persist if the image is saved in TIFF format. + * Add a "HideInfo" property (e.g. set("HideInfo","true")) to + * prevent the properties from being displayed by the + * Image/Show Info command. + */ + public void setProp(String key, String value) { + if (key==null) + return; + if (imageProperties==null) + imageProperties = new Properties(); + if (value==null || value.length()==0) + imageProperties.remove(key); + else + imageProperties.setProperty(key, value); + } + + /** Saves a persistent numeric propery. The property is + * removed if 'value' is NaN. + * @see #getNumericProp + */ + public void setProp(String key, double value) { + String svalue = ""+value; + if (svalue.endsWith(".0")) + svalue = svalue.substring(0,svalue.length()-2); + setProp(key, Double.isNaN(value)?null:svalue); + } + + /** Returns as a string the image property associated with the + * specified key or null if the property is not found. + * @see #setProp + * @see #getNumericProp + * @see #getStringProperty + */ + public String getProp(String key) { + if (imageProperties==null) + return getStringProperty(key); + else { + String value = imageProperties.getProperty(key); + if (value==null) + value = getStringProperty(key); + return value; + } + } + + /** Returns the numeric property associated with the specified key + * or NaN if the property is not found. + * @see #setProp(String,double) + * @see #getProp + */ + public double getNumericProp(String key) { + if (imageProperties==null) + return Double.NaN; + else + return Tools.parseDouble(getProp(key), Double.NaN); + } + + /** Used for saving string properties in TIFF header. */ + public String[] getPropertiesAsArray() { + if (imageProperties==null || imageProperties.size()==0) + return null; + String props[] = new String[imageProperties.size()*2]; + int index = 0; + for (Enumeration en=imageProperties.keys(); en.hasMoreElements();) { + String key = (String)en.nextElement(); + String value = imageProperties.getProperty(key); + props[index++] = key; + props[index++] = value; + } + return props; + } + + /** Returns information displayed by Image/Show Info command. */ + public String getPropsInfo() { + if (imageProperties==null || imageProperties.size()==0) + return "0"; + String info2 = ""; + for (Enumeration en=imageProperties.keys(); en.hasMoreElements();) { + String key = (String)en.nextElement(); + if (info2.length()>50) { + info2 += "..."; + break; + } else + info2 += " " + key; + } + if (info2.length()>1) + info2 = " (" + info2.substring(1) + ")"; + return imageProperties.size() + info2; + } + + /** Creates a set of image properties from an array of strings. + * @see #getPropertiesAsArray + * @see #getProp(String) + * @see #setProp(String,String) + */ + public void setProperties(String[] props) { + imageProperties = null; + if (props==null) + return; + //IJ.log("setProperties: "+props.length+" "+getTitle()); + for (int i=0; i>16; + int g = (c&0xff00)>>8; + int b = c&0xff; + pvalue[0] = r; + pvalue[1] = g; + pvalue[2] = b; + break; + case GRAY16: case GRAY32: + if (ip!=null) pvalue[0] = ip.getPixel(x, y); + break; + } + return pvalue; + } + + /** Returns an empty image stack that has the same + width, height and color table as this image. */ + public ImageStack createEmptyStack() { + ColorModel cm; + if (ip!=null) + cm = ip.getColorModel(); + else + cm = createLut().getColorModel(); + return new ImageStack(width, height, cm); + } + + /** Returns the image stack. The stack may have only + one slice. After adding or removing slices, call + setStack() to update the image and + the window that is displaying it. + @see #setStack + */ + public ImageStack getStack() { + ImageStack s; + if (stack==null) { + s = createEmptyStack(); + ImageProcessor ip2 = getProcessor(); + if (ip2==null) + return s; + String label = getProp("Slice_Label"); + if (label==null) { + String info = (String)getProperty("Info"); + label = info!=null?getTitle()+"\n"+info:null; // DICOM metadata + } + s.addSlice(label, ip2); + s.update(ip2); + this.stack = s; + ip = ip2; + oneSliceStack = true; + setCurrentSlice(1); + } else { + s = stack; + if (ip!=null) { + Calibration cal = getCalibration(); + if (cal.calibrated()) + ip.setCalibrationTable(cal.getCTable()); + else + ip.setCalibrationTable(null); + } + s.update(ip); + } + if (roi!=null) + s.setRoi(roi.getBounds()); + else + s.setRoi(null); + return s; + } + + /** Returns the base image stack. */ + public ImageStack getImageStack() { + if (stack==null) + return getStack(); + else { + stack.update(ip); + return stack; + } + } + + /** Returns the current stack index (one-based) or 1 if this is a single image. */ + public int getCurrentSlice() { + if (currentSlice<1) setCurrentSlice(1); + if (currentSlice>getStackSize()) + setCurrentSlice(getStackSize()); + return currentSlice; + } + + final void setCurrentSlice(int slice) { + currentSlice = slice; + int stackSize = getStackSize(); + if (nChannels==stackSize) updatePosition(currentSlice, 1, 1); + if (nSlices==stackSize) updatePosition(1, currentSlice, 1); + if (nFrames==stackSize) updatePosition(1, 1, currentSlice); + } + + public int getChannel() { + return position[0]; + } + + public int getSlice() { + return position[1]; + } + + public int getFrame() { + return position[2]; + } + + public void killStack() { + setStackNull(); + trimProcessor(); + } + + /** Sets the current hyperstack position and updates the display, + where 'channel', 'slice' and 'frame' are one-based indexes. */ + public void setPosition(int channel, int slice, int frame) { + //IJ.log("setPosition: "+channel+" "+slice+" "+frame+" "+noUpdateMode); + verifyDimensions(); + if (channel<0) channel=0; + if (slice<0) slice=0; + if (frame<0) frame=0; + if (channel==0) channel=getC(); + if (slice==0) slice=getZ(); + if (frame==0) frame=getT(); + if (channel>nChannels) channel=nChannels; + if (slice>nSlices) slice=nSlices; + if (frame>nFrames) frame=nFrames; + if (isDisplayedHyperStack()) + ((StackWindow)win).setPosition(channel, slice, frame); + else { + boolean channelChanged = channel!=getChannel(); + setSlice((frame-1)*nChannels*nSlices + (slice-1)*nChannels + channel); + updatePosition(channel, slice, frame); + if (channelChanged && isComposite() && !noUpdateMode) + updateImage(); + } + } + + /** Sets the current hyperstack position without updating the display, + where 'channel', 'slice' and 'frame' are one-based indexes. */ + public void setPositionWithoutUpdate(int channel, int slice, int frame) { + noUpdateMode = true; + setPosition(channel, slice, frame); + noUpdateMode = false; + } + + /** Sets the hyperstack channel position (one based). */ + public void setC(int channel) { + setPosition(channel, getZ(), getT()); + } + + /** Sets the hyperstack slice position (one based). */ + public void setZ(int slice) { + setPosition(getC(), slice, getT()); + } + + /** Sets the hyperstack frame position (one based). */ + public void setT(int frame) { + setPosition(getC(), getZ(), frame); + } + + /** Returns the current hyperstack channel position. */ + public int getC() { + return position[0]; + } + + /** Returns the current hyperstack slice position. */ + public int getZ() { + return position[1]; + } + + /** Returns the current hyperstack frame position. */ + public int getT() { + return position[2]; + } + + /** Returns that stack index (one-based) corresponding to the specified position. */ + public int getStackIndex(int channel, int slice, int frame) { + if (channel<1) channel = 1; + if (channel>nChannels) channel = nChannels; + if (slice<1) slice = 1; + if (slice>nSlices) slice = nSlices; + if (frame<1) frame = 1; + if (frame>nFrames) frame = nFrames; + return (frame-1)*nChannels*nSlices + (slice-1)*nChannels + channel; + } + + /* Hack needed to make the HyperStackReducer work. */ + public void resetStack() { + if (currentSlice==1 && stack!=null && stack.size()>0) { + ColorModel cm = ip.getColorModel(); + double min = ip.getMin(); + double max = ip.getMax(); + ImageProcessor ip2 = stack.getProcessor(1); + if (ip2!=null) { + ip = ip2; + ip.setColorModel(cm); + ip.setMinAndMax(min, max); + } + } + } + + /** Set the current hyperstack position based on the stack index 'n' (one-based). */ + public void setPosition(int n) { + int[] pos = convertIndexToPosition(n); + setPosition(pos[0], pos[1], pos[2]); + } + + /** Converts the stack index 'n' (one-based) into a hyperstack position (channel, slice, frame). */ + public int[] convertIndexToPosition(int n) { + if (n<1 || n>getStackSize()) + throw new IllegalArgumentException("n out of range: "+n); + int[] position = new int[3]; + int[] dim = getDimensions(); + position[0] = ((n-1)%dim[2])+1; + position[1] = (((n-1)/dim[2])%dim[3])+1; + position[2] = (((n-1)/(dim[2]*dim[3]))%dim[4])+1; + return position; + } + + /** Displays the specified stack image, where 1<=n<=stackSize. + * Does nothing if this image is not a stack. + * @see #setPosition + * @see #setC + * @see #setZ + * @see #setT + */ + public synchronized void setSlice(int n) { + if (stack==null || (n==currentSlice&&ip!=null)) { + if (!noUpdateMode) + updateAndRepaintWindow(); + return; + } + if (n>=1 && n<=stack.size()) { + Roi roi = getRoi(); + if (roi!=null) + roi.endPaste(); + if (isProcessor()) { + if (currentSlice==0) currentSlice=1; + stack.setPixels(ip.getPixels(),currentSlice); + } + setCurrentSlice(n); + Object pixels = null; + Overlay overlay2 = null; + if (stack.isVirtual() && !((stack instanceof FileInfoVirtualStack)||(stack instanceof AVI_Reader))) { + ImageProcessor ip2 = stack.getProcessor(currentSlice); + overlay2 = ip2!=null?ip2.getOverlay():null; + if (overlay2!=null) + setOverlay(overlay2); + if (stack instanceof VirtualStack) { + Properties props = ((VirtualStack)stack).getProperties(); + if (props!=null) + setProperty("FHT", props.get("FHT")); + } + if (ip2!=null) pixels=ip2.getPixels(); + } else + pixels = stack.getPixels(currentSlice); + if (ip!=null && pixels!=null) { + try { + ip.setPixels(pixels); + ip.setSnapshotPixels(null); + } catch(Exception e) {} + } else { + ImageProcessor ip2 = stack.getProcessor(n); + if (ip2!=null) ip = ip2; + } + if (compositeImage && getCompositeMode()==IJ.COMPOSITE && ip!=null) { + int channel = getC(); + if (channel>0 && channel<=getNChannels()) + ip.setLut(((CompositeImage)this).getChannelLut(channel)); + } + if (win!=null && win instanceof StackWindow) + ((StackWindow)win).updateSliceSelector(); + if (Prefs.autoContrast && nChannels==1 && imageType!=COLOR_RGB) { + (new ContrastEnhancer()).stretchHistogram(ip,0.35,ip.getStats()); + ContrastAdjuster.update(); + //IJ.showStatus(n+": min="+ip.getMin()+", max="+ip.getMax()); + } + if (imageType==COLOR_RGB) + ContrastAdjuster.update(); + if (!noUpdateMode) + updateAndRepaintWindow(); + else + img = null; + } + } + + /** Displays the specified stack image (1<=n<=stackSize) + without updating the display. */ + public void setSliceWithoutUpdate(int n) { + noUpdateMode = true; + setSlice(n); + noUpdateMode = false; + } + + /** Returns the current selection, or null if there is no selection. */ + public Roi getRoi() { + return roi; + } + + /** Assigns the specified ROI to this image and displays it. Any existing + ROI is deleted if roi is null or its width or height is zero. */ + public void setRoi(Roi newRoi) { + setRoi(newRoi, true); + } + + /** Assigns 'newRoi' to this image and displays it if 'updateDisplay' is true. */ + public void setRoi(Roi newRoi, boolean updateDisplay) { + if (newRoi==null) { + deleteRoi(); + return; + } + if (Recorder.record) { + Recorder recorder = Recorder.getInstance(); + if (recorder!=null) recorder.imageUpdated(this); + } + Rectangle bounds = newRoi.getBounds(); + if (newRoi.isVisible()) { + if ((newRoi instanceof Arrow) && newRoi.getState()==Roi.CONSTRUCTING && bounds.width==0 && bounds.height==0) { + deleteRoi(); + roi = newRoi; + return; + } + if (newRoi==null) { + deleteRoi(); + return; + } + ImagePlus imp = newRoi.getImage(); + if (imp!=null && imp.getID()!=getID()) + newRoi = (Roi)newRoi.clone(); + newRoi.setImage(null); + } + if (bounds.width==0 && bounds.height==0 && !(newRoi.getType()==Roi.POINT||newRoi.getType()==Roi.LINE)) { + deleteRoi(); + return; + } + roi = newRoi; + if (ip!=null) { + ip.setMask(null); + if (roi.isArea()) + ip.setRoi(bounds); + else + ip.resetRoi(); + } + roi.setImage(this); + if ((roi instanceof PointRoi) && ((PointRoi)roi).addToOverlay()) { + IJ.run(this, "Add Selection...", ""); + roi = null; + return; + } + if (updateDisplay) + draw(); + if (roi!=null) + roi.notifyListeners(RoiListener.CREATED); + } + + /** Creates a rectangular selection. */ + public void setRoi(int x, int y, int width, int height) { + setRoi(new Rectangle(x, y, width, height)); + } + + /** Creates a rectangular selection. */ + public void setRoi(Rectangle r) { + setRoi(new Roi(r.x, r.y, r.width, r.height)); + } + + /** Starts the process of creating a new selection, where sx and sy are the + starting screen coordinates. The selection type is determined by which tool in + the tool bar is active. The user interactively sets the selection size and shape. */ + public void createNewRoi(int sx, int sy) { + Roi previousRoi = roi; + deleteRoi(); //also saves the roi as Roi.previousRoi if non-null + Roi prevRoi = Roi.getPreviousRoi(); + if (prevRoi != null) + prevRoi.setImage(previousRoi==null ? null : this); //with 'this' it will be recalled in case of ESC + switch (Toolbar.getToolId()) { + case Toolbar.RECTANGLE: + if (Toolbar.getRectToolType()==Toolbar.ROTATED_RECT_ROI) + roi = new RotatedRectRoi(sx, sy, this); + else + roi = new Roi(sx, sy, this, Toolbar.getRoundRectArcSize()); + break; + case Toolbar.OVAL: + if (Toolbar.getOvalToolType()==Toolbar.ELLIPSE_ROI) + roi = new EllipseRoi(sx, sy, this); + else + roi = new OvalRoi(sx, sy, this); + break; + case Toolbar.POLYGON: + case Toolbar.POLYLINE: + case Toolbar.ANGLE: + roi = new PolygonRoi(sx, sy, this); + break; + case Toolbar.FREEROI: + case Toolbar.FREELINE: + roi = new FreehandRoi(sx, sy, this); + break; + case Toolbar.LINE: + if ("arrow".equals(Toolbar.getToolName())) + roi = new Arrow(sx, sy, this); + else + roi = new Line(sx, sy, this); + break; + case Toolbar.TEXT: + roi = new TextRoi(sx, sy, this); + ((TextRoi)roi).setPreviousTextRoi(previousRoi); + break; + case Toolbar.POINT: + roi = new PointRoi(sx, sy, this); + if (Prefs.pointAddToOverlay) { + int measurements = Analyzer.getMeasurements(); + if (!(Prefs.pointAutoMeasure && (measurements&Measurements.ADD_TO_OVERLAY)!=0)) + IJ.run(this, "Add Selection...", ""); + Overlay overlay2 = getOverlay(); + if (overlay2!=null) + overlay2.drawLabels(!Prefs.noPointLabels); + Prefs.pointAddToManager = false; + } + if (Prefs.pointAutoMeasure || (Prefs.pointAutoNextSlice&&!Prefs.pointAddToManager)) + IJ.run(this, "Measure", ""); + if (Prefs.pointAddToManager) { + IJ.run(this, "Add to Manager ", ""); + ImageCanvas ic = getCanvas(); + if (ic!=null) { + RoiManager rm = RoiManager.getInstance(); + if (rm!=null) { + if (Prefs.noPointLabels) + rm.runCommand("show all without labels"); + else + rm.runCommand("show all with labels"); + } + } + } + if (Prefs.pointAutoNextSlice && getStackSize()>1) { + boolean order = Prefs.reverseNextPreviousOrder; + Prefs.reverseNextPreviousOrder = true; + IJ.run(this, "Next Slice [>]", ""); + Prefs.reverseNextPreviousOrder = order; + deleteRoi(); + } + break; + } + if (roi!=null) + roi.notifyListeners(RoiListener.CREATED); + } + + /** Deletes the current region of interest. Makes a copy of the ROI + so it can be recovered by Edit/Selection/Restore Selection. */ + public void deleteRoi() { + if (roi==null) + return; + saveRoi(); + if (!(IJ.altKeyDown()||IJ.shiftKeyDown())) { + RoiManager rm = RoiManager.getRawInstance(); + if (rm!=null) + rm.deselect(roi); + } + if (roi!=null) + roi.notifyListeners(RoiListener.DELETED); + roi = null; + if (ip!=null) + ip.resetRoi(); + draw(); + } + + public boolean okToDeleteRoi() { + if (roi!=null && (roi instanceof PointRoi) && getWindow()!=null && ((PointRoi)roi).promptBeforeDeleting()) { + int npoints = ((PolygonRoi)roi).getNCoordinates(); + int counters = ((PointRoi)roi).getNCounters(); + String msg = "Delete this multi-point selection ("+npoints+" points, "+counters+" counter"+(counters>1?"s":"")+")?"; + GenericDialog gd=new GenericDialog("Delete Points?"); + gd.addMessage(msg+"\nRestore using Edit>Selection>Restore Selection."); + gd.addHelp(PointToolOptions.help); + gd.setOKLabel("Keep"); + gd.setCancelLabel("Delete"); + gd.showDialog(); + if (gd.wasOKed()) + return false; + } + return true; + } + + /** Deletes the current region of interest. */ + public void killRoi() { + deleteRoi(); + } + + /** Deletes the current region of interest. */ + public void resetRoi() { + deleteRoi(); + } + + public void saveRoi() { + Roi roi2 = roi; + if (roi2!=null) { + roi2.endPaste(); + Rectangle r = roi2.getBounds(); + if ((r.width>0 || r.height>0)) { + Roi.setPreviousRoi(roi2); + if (IJ.debugMode) IJ.log("saveRoi: "+roi2); + } + if ((roi2 instanceof PointRoi) && ((PointRoi)roi2).promptBeforeDeleting()) { + PointRoi.savedPoints = (PointRoi)roi2.clone(); + if (IJ.debugMode) IJ.log("saveRoi: saving multi-point selection"); + } + } + } + + public void restoreRoi() { + if (Toolbar.getToolId()==Toolbar.POINT && PointRoi.savedPoints!=null) { + roi = (Roi)PointRoi.savedPoints.clone(); + draw(); + roi.notifyListeners(RoiListener.MODIFIED); + return; + } + Roi previousRoi = Roi.getPreviousRoi(); + if (previousRoi!=null) { + Roi pRoi = previousRoi; + Rectangle r = pRoi.getBounds(); + if (r.width<=width||r.height<=height||(r.x=width || r.y>=height || (r.x+r.width)<0 || (r.y+r.height)<0) // does it need to be moved? + roi.setLocation((width-r.width)/2, (height-r.height)/2); + else if (r.width==width && r.height==height) // is it the same size as the image + roi.setLocation(0, 0); + draw(); + roi.notifyListeners(RoiListener.MODIFIED); + } + } + } + + boolean isSmaller(Roi r) { + ImageProcessor mask = r.getMask(); + if (mask==null) return false; + mask.setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE); + ImageStatistics stats = ImageStatistics.getStatistics(mask, MEAN+LIMIT, null); + return stats.area<=width*height; + } + + /** Implements the File/Revert command. */ + public void revert() { + if (getStackSize()>1 && getStack().isVirtual()) { + int thisSlice = currentSlice; + currentSlice = 0; + setSlice(thisSlice); + return; + } + FileInfo fi = getOriginalFileInfo(); + boolean isFileInfo = fi!=null && fi.fileFormat!=FileInfo.UNKNOWN; + if (!isFileInfo && url==null) + return; + if (fi.directory==null && url==null) + return; + if (ij!=null && changes && isFileInfo && !Interpreter.isBatchMode() && !IJ.isMacro() && !IJ.altKeyDown()) { + if (!IJ.showMessageWithCancel("Revert?", "Revert to saved version of\n\""+getTitle()+"\"?")) + return; + } + Roi saveRoi = null; + if (roi!=null) { + roi.endPaste(); + saveRoi = (Roi)roi.clone(); + } + trimProcessor(); + new FileOpener(fi).revertToSaved(this); + if (Prefs.useInvertingLut && getBitDepth()==8 && ip!=null && !ip.isInvertedLut()&& !ip.isColorLut()) + invertLookupTable(); + if (getProperty("FHT")!=null) { + properties.remove("FHT"); + if (getTitle().startsWith("FFT of ")) + setTitle(getTitle().substring(7)); + } + ContrastAdjuster.update(); + if (saveRoi!=null) setRoi(saveRoi); + repaintWindow(); + IJ.showStatus(""); + changes = false; + notifyListeners(UPDATED); + } + + void revertStack(FileInfo fi) { + String path = null; + String url2 = null; + if (url!=null && !url.equals("")) { + path = url; + url2 = url; + } else if (fi!=null && !((fi.directory==null||fi.directory.equals("")))) { + path = fi.getFilePath(); + } else if (fi!=null && fi.url!=null && !fi.url.equals("")) { + path = fi.url; + url2 = fi.url; + } else + return; + IJ.showStatus("Loading: " + path); + ImagePlus imp = IJ.openImage(path); + if (imp!=null) { + int n = imp.getStackSize(); + int c = imp.getNChannels(); + int z = imp.getNSlices(); + int t = imp.getNFrames(); + if (z==n || t==n || (c==getNChannels()&&z==getNSlices()&&t==getNFrames())) { + setCalibration(imp.getCalibration()); + setStack(imp.getStack(), c, z, t); + } else { + ImageWindow win = getWindow(); + Point loc = null; + if (win!=null) loc = win.getLocation(); + changes = false; + close(); + FileInfo fi2 = imp.getOriginalFileInfo(); + if (fi2!=null && (fi2.url==null || fi2.url.length()==0)) { + fi2.url = url2; + imp.setFileInfo(fi2); + } + ImageWindow.setNextLocation(loc); + imp.show(); + } + } + } + + /** Returns a FileInfo object containing information, including the + pixel array, needed to save this image. Use getOriginalFileInfo() + to get a copy of the FileInfo object used to open the image. + @see ij.io.FileInfo + @see #getOriginalFileInfo + @see #setFileInfo + */ + public FileInfo getFileInfo() { + FileInfo fi = new FileInfo(); + fi.width = width; + fi.height = height; + fi.nImages = getStackSize(); + if (compositeImage) + fi.nImages = getImageStackSize(); + fi.whiteIsZero = isInvertedLut(); + fi.intelByteOrder = false; + if (fi.nImages==1 && ip!=null) + fi.pixels = ip.getPixels(); + else if (stack!=null) + fi.pixels = stack.getImageArray(); + Calibration cal = getCalibration(); + if (cal.scaled()) { + fi.pixelWidth = cal.pixelWidth; + fi.pixelHeight = cal.pixelHeight; + fi.unit = cal.getUnit(); + } + if (fi.nImages>1) + fi.pixelDepth = cal.pixelDepth; + fi.frameInterval = cal.frameInterval; + if (cal.calibrated()) { + fi.calibrationFunction = cal.getFunction(); + fi.coefficients = cal.getCoefficients(); + fi.valueUnit = cal.getValueUnit(); + } else if (!Calibration.DEFAULT_VALUE_UNIT.equals(cal.getValueUnit())) + fi.valueUnit = cal.getValueUnit(); + + switch (imageType) { + case GRAY8: case COLOR_256: + LookUpTable lut = createLut(); + boolean customLut = !lut.isGrayscale() || (ip!=null&&!ip.isDefaultLut()); + if (imageType==COLOR_256 || customLut) + fi.fileType = FileInfo.COLOR8; + else + fi.fileType = FileInfo.GRAY8; + addLut(lut, fi); + break; + case GRAY16: + if (compositeImage && fi.nImages==3) { + if ("Red".equals(getStack().getSliceLabel(1))) + fi.fileType = fi.RGB48; + else + fi.fileType = fi.GRAY16_UNSIGNED; + } else + fi.fileType = fi.GRAY16_UNSIGNED; + if (!compositeImage) { + lut = createLut(); + if (!lut.isGrayscale() || (ip!=null&&!ip.isDefaultLut())) + addLut(lut, fi); + } + break; + case GRAY32: + fi.fileType = fi.GRAY32_FLOAT; + if (!compositeImage) { + lut = createLut(); + if (!lut.isGrayscale() || (ip!=null&&!ip.isDefaultLut())) + addLut(lut, fi); + } + break; + case COLOR_RGB: + fi.fileType = fi.RGB; + break; + default: + } + return fi; + } + + private void addLut(LookUpTable lut, FileInfo fi) { + fi.lutSize = lut.getMapSize(); + fi.reds = lut.getReds(); + fi.greens = lut.getGreens(); + fi.blues = lut.getBlues(); + } + + /** Returns the FileInfo object that was used to open this image. + Returns null for images created using the File/New command. + @see ij.io.FileInfo + @see #getFileInfo + */ + public FileInfo getOriginalFileInfo() { + if (fileInfo==null & url!=null) { + fileInfo = new FileInfo(); + fileInfo.width = width; + fileInfo.height = height; + fileInfo.url = url; + fileInfo.directory = null; + } + return fileInfo; + } + + /** Used by ImagePlus to monitor loading of images. */ + public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) { + imageUpdateY = y; + imageUpdateW = w; + if ((flags & ERROR) != 0) { + errorLoadingImage = true; + return false; + } + imageLoaded = (flags & (ALLBITS|FRAMEBITS|ABORT)) != 0; + return !imageLoaded; + } + + /** Sets the ImageProcessor, Roi, AWT Image and stack image + arrays to null. Does nothing if the image is locked. */ + public synchronized void flush() { + notifyListeners(CLOSED); + if (locked || ignoreFlush) return; + ip = null; + if (roi!=null) roi.setImage(null); + roi = null; + if (stack!=null && stack.viewers(-1)<=0) { + Object[] arrays = stack.getImageArray(); + if (arrays!=null) { + for (int i=0; istackSize) s2 = stackSize; + if (s1>s2) {s1=1; s2=stackSize;} + return new Duplicator().run(this, (int)s1, (int)s2); + } + } + + /** Returns an array of cropped images based on the provided + * list of rois. 'options' applies with stacks and can be "stack", + * "slice" or a range (e.g., "20-30"). + * @see #crop(ij.gui.Roi[]) + */ + public ImagePlus[] crop(Roi[] rois, String options) { + int nRois = rois.length; + ImagePlus[] cropImps = new ImagePlus[nRois]; + for (int i=0; i1) { + int position = cropRoi.getPosition(); + this.setSlice(position); // no effect if roi position is undefined (=0), ok + } + this.setRoi(cropRoi); + ImagePlus cropped = this.crop(options); + if (cropRoi.getType()!=Roi.RECTANGLE) { + Roi cropRoi2 = (Roi)cropRoi.clone(); + cropRoi2.setLocation(0,0); + cropped.setRoi(cropRoi2); + } + String name2 = IJ.pad(i+1,3)+"_"+this.getTitle(); + cropped.setTitle(name!=null?name:name2); + cropped.setOverlay(null); + cropImps[i] = cropped; + } + return cropImps; + } + + /** Multi-roi cropping with default "slice" option. */ + public ImagePlus[] crop(Roi[] rois) { + return this.crop(rois, "slice"); + } + + /** Saves the contents of the ROIs in this overlay as separate images, + * where 'directory' is the directory path and 'format' is "tif", "png" or "jpg". + * Set 'format' to "show" and the images will be displayed in a stack + * and not saved. + */ + public void cropAndSave(Roi[] rois, String directory, String format) { + ImagePlus[] images = crop(rois); + if (format==null) format = ""; + if (format.contains("show")) { + ImageStack stack = ImageStack.create(images); + new ImagePlus("CROPPED_"+getTitle(),stack).show(); + return; + } + String fileFormat = "tif"; + if (format.contains("png")) fileFormat = "png"; + if (format.contains("jpg")) fileFormat = "jpg"; + for (int i=0; icut is true. */ + public void copy(boolean cut) { + Roi roi = getRoi(); + if (roi!=null && !roi.isArea()) + roi = null; + if (cut && roi==null && !IJ.isMacro()) { + IJ.error("Edit>Cut", "This command requires an area selection"); + return; + } + boolean batchMode = Interpreter.isBatchMode(); + String msg = (cut)?"Cut":"Copy"; + if (!batchMode) IJ.showStatus(msg+ "ing..."); + ImageProcessor ip = getProcessor(); + ImageProcessor ip2; + ip2 = ip.crop(); + clipboard = new ImagePlus("Clipboard", ip2); + if (roi!=null) + clipboard.setRoi((Roi)roi.clone()); + if (cut) { + ip.snapshot(); + ip.setColor(Toolbar.getBackgroundColor()); + ip.fill(); + if (roi!=null && roi.getType()!=Roi.RECTANGLE) { + getMask(); + ip.reset(ip.getMask()); + } setColor(Toolbar.getForegroundColor()); + Undo.setup(Undo.FILTER, this); + updateAndDraw(); + } + int bytesPerPixel = 1; + switch (clipboard.getType()) { + case ImagePlus.GRAY16: bytesPerPixel = 2; break; + case ImagePlus.GRAY32: case ImagePlus.COLOR_RGB: bytesPerPixel = 4; + } + if (!batchMode) { + msg = (cut)?"Cut":"Copy"; + IJ.showStatus(msg + ": " + (clipboard.getWidth()*clipboard.getHeight()*bytesPerPixel)/1024 + "k"); + } + } + + /** Inserts the contents of the internal clipboard into this image. If there + is a selection the same size as the image on the clipboard, the image is inserted + into that selection, otherwise the selection is inserted into the center of the image.*/ + public void paste() { + if (clipboard==null) + return; + int cType = clipboard.getType(); + int iType = getType(); + int w = clipboard.getWidth(); + int h = clipboard.getHeight(); + Roi cRoi = clipboard.getRoi(); + Rectangle r = null; + Rectangle cr = null; + Roi roi = getRoi(); + if (roi!=null) + r = roi.getBounds(); + if (cRoi!=null) + cr = cRoi.getBounds(); + if (cr==null) + cr = new Rectangle(0, 0, w, h); + if (r==null || (cr.width!=r.width || cr.height!=r.height)) { + // Create a new roi centered on visible part of image, or centered on image if clipboard is >= image + ImageCanvas ic = win!=null?ic = win.getCanvas():null; + Rectangle srcRect = ic!=null?ic.getSrcRect():new Rectangle(0,0,width,height); + int xCenter = w>=width ? width/2 : srcRect.x + srcRect.width/2; + int yCenter = h>=height ? height/2 : srcRect.y + srcRect.height/2; + if (cRoi!=null && cRoi.getType()!=Roi.RECTANGLE) { + cRoi.setImage(this); + cRoi.setLocation(xCenter-w/2, yCenter-h/2); + setRoi(cRoi); + } else + setRoi(xCenter-w/2, yCenter-h/2, w, h); + roi = getRoi(); + } + if (IJ.isMacro()) { + //non-interactive paste + int pasteMode = Roi.getCurrentPasteMode(); + boolean nonRect = roi.getType()!=Roi.RECTANGLE; + ImageProcessor ip = getProcessor(); + if (nonRect) ip.snapshot(); + r = roi.getBounds(); + int xoffset = cr.x<0?-cr.x:0; + int yoffset = cr.y<0?-cr.y:0; + ip.copyBits(clipboard.getProcessor(), r.x+xoffset, r.y+yoffset, pasteMode); + if (nonRect) { + ImageProcessor mask = roi.getMask(); + ip.setMask(mask); + ip.setRoi(roi.getBounds()); + ip.reset(ip.getMask()); + } + updateAndDraw(); + } else if (roi!=null) { + roi.startPaste(clipboard); + Undo.setup(Undo.PASTE, this); + } + changes = true; + } + + /** Inserts the contents of the internal clipboard at the + specified location, without updating the display. */ + public void paste(int x, int y) { + paste(x, y, null); + } + + /** Copies the contents of the internal clipboard to the + * specified location using the specified transfer mode + * ("Copy", "Blend", "Average", "Difference", "Transparent", + * "Transparent2", "AND", "OR", "XOR", "Add", "Subtract", + * "Multiply", or "Divide"). The display is not updating. + */ + public void paste(int x, int y, String mode) { + if (clipboard==null) + return; + Roi roi = clipboard.getRoi(); + boolean nonRect = roi!=null && roi.getType()!=Roi.RECTANGLE; + if (nonRect) + ip.snapshot(); + if (mode==null) + ip.insert(clipboard.getProcessor(), x, y); + else { + int pasteMode = IJ.stringToPasteMode(mode); + ip.copyBits(clipboard.getProcessor(), x, y, pasteMode); + } + if (nonRect) { + ImageProcessor mask = roi.getMask(); + ip.setRoi(x, y, mask.getWidth(), mask.getHeight()); + ip.setMask(mask); + ip.reset(ip.getMask()); + } + } + + /** Returns the internal clipboard or null if the internal clipboard is empty. */ + public static ImagePlus getClipboard() { + return clipboard; + } + + /** Clears the internal clipboard. */ + public static void resetClipboard() { + clipboard = null; + } + + /** Copies the contents of the current selection, or the entire + image if there is no selection, to the system clipboard. */ + public void copyToSystem() { + Clipboard.copyToSystem(this); + } + + protected void notifyListeners(final int id) { + if (temporary) + return; + final ImagePlus imp = this; + EventQueue.invokeLater(new Runnable() { + public void run() { + for (int i=0; i=1 && imageType!=COLOR_RGB && (this instanceof CompositeImage); + } + + /** Returns the display mode (IJ.COMPOSITE, IJ.COLOR + or IJ.GRAYSCALE) if this is a CompositeImage, otherwise returns -1. */ + public int getCompositeMode() { + if (isComposite()) + return ((CompositeImage)this).getMode(); + else + return -1; + } + + /** Sets the display range of the current channel. With non-composite + images it is identical to ip.setMinAndMax(min, max). */ + public void setDisplayRange(double min, double max) { + if (ip!=null) + ip.setMinAndMax(min, max); + } + + public double getDisplayRangeMin() { + return ip.getMin(); + } + + public double getDisplayRangeMax() { + return ip.getMax(); + } + + /** Sets the display range of specified channels in an RGB image, where 4=red, + 2=green, 1=blue, 6=red+green, etc. With non-RGB images, this method is + identical to setDisplayRange(min, max). This method is used by the + Image/Adjust/Color Balance tool . */ + public void setDisplayRange(double min, double max, int channels) { + if (ip instanceof ColorProcessor) + ((ColorProcessor)ip).setMinAndMax(min, max, channels); + else + ip.setMinAndMax(min, max); + } + + public void resetDisplayRange() { + if (imageType==GRAY16 && default16bitDisplayRange>=8 && default16bitDisplayRange<=16 && !(getCalibration().isSigned16Bit())) + ip.setMinAndMax(0, Math.pow(2,default16bitDisplayRange)-1); + else + ip.resetMinAndMax(); + } + + /** Returns 'true' if this image is thresholded. */ + public boolean isThreshold() { + return ip!=null && ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD; + } + + /** Set the default 16-bit display range, where 'bitDepth' must be 0 (auto-scaling), + 8 (0-255), 10 (0-1023), 12 (0-4095, 14 (0-16383), 15 (0-32767) or 16 (0-65535). */ + public static void setDefault16bitRange(int bitDepth) { + if (!(bitDepth==8 || bitDepth==10 || bitDepth==12 || bitDepth==14 || bitDepth==15 || bitDepth==16)) + bitDepth = 0; + default16bitDisplayRange = bitDepth; + } + + /** Returns the default 16-bit display range, 0 (auto-scaling), 8, 10, 12, 14, 15 or 16. */ + public static int getDefault16bitRange() { + return default16bitDisplayRange; + } + + public void updatePosition(int c, int z, int t) { + position[0] = c; + position[1] = z; + position[2] = t; + } + + /** Returns a "flattened" version of this image, or stack slice, in RGB format. */ + public ImagePlus flatten() { + if (IJ.debugMode) IJ.log("flatten"); + ImagePlus impCopy = this; + if (getStackSize()>1) + impCopy = crop("whole-slice"); + ImagePlus imp2 = impCopy.createImagePlus(); + imp2.setOverlay(impCopy.getOverlay()); + imp2.setTitle(flattenTitle); + ImageCanvas ic2 = new ImageCanvas(imp2); + imp2.flatteningCanvas = ic2; + imp2.setRoi(getRoi()); + Overlay overlay2 = getOverlay(); + if (overlay2!=null && imp2.getRoi()!=null && !(imp2.getRoi() instanceof PointRoi)) { + imp2.deleteRoi(); + if (getWindow()!=null) IJ.wait(100); + } + setPointScale(imp2.getRoi(), overlay2); + ImageCanvas ic = getCanvas(); + if (ic!=null) + ic2.setShowAllList(ic.getShowAllList()); + BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = (Graphics2D)bi.getGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + antialiasRendering?RenderingHints.VALUE_ANTIALIAS_ON:RenderingHints.VALUE_ANTIALIAS_OFF); + g.drawImage(getImage(), 0, 0, null); + ic2.paint(g); + imp2.flatteningCanvas = null; + ImagePlus imp3 = new ImagePlus("Flat_"+getTitle(), new ColorProcessor(bi)); + imp3.copyScale(this); + imp3.setProperty("Info", getProperty("Info")); + imp3.setProperties(getPropertiesAsArray()); + return imp3; + } + + /** Flattens all slices of this stack or HyperStack.
+ * @throws UnsupportedOperationException if this image
+ * does not have an overlay and the RoiManager overlay is null
+ * or Java version is less than 1.6. + * Copied from OverlayCommands and modified by Marcel Boeglin + * on 2014.01.08 to work with HyperStacks. + */ + public void flattenStack() { + if (IJ.debugMode) IJ.log("flattenStack"); + if (getStackSize()==1) + throw new UnsupportedOperationException("Image stack required"); + boolean composite = isComposite(); + if (getBitDepth()!=24) + new ImageConverter(this).convertToRGB(); + Overlay overlay1 = getOverlay(); + Overlay roiManagerOverlay = null; + boolean roiManagerShowAllMode = !Prefs.showAllSliceOnly; + ImageCanvas ic = getCanvas(); + if (ic!=null) + roiManagerOverlay = ic.getShowAllList(); + setOverlay(null); + if (roiManagerOverlay!=null) { + RoiManager rm = RoiManager.getInstance(); + if (rm!=null) + rm.runCommand("show none"); + } + Overlay overlay2 = overlay1!=null?overlay1:roiManagerOverlay; + if (composite && overlay2==null) + return; + if (overlay2==null || overlay2.size()==0) + throw new UnsupportedOperationException("A non-empty overlay is required"); + ImageStack stack2 = getStack(); + boolean showAll = overlay1!=null?false:roiManagerShowAllMode; + if (isHyperStack()) { + int Z = getNSlices(); + for (int z=1; z<=Z; z++) { + for (int t=1; t<=getNFrames(); t++) { + int s = z + (t-1)*Z; + flattenImage(stack2, s, overlay2.duplicate(), showAll, z, t); + } + } + } else { + for (int s=1; s<=stack2.getSize(); s++) { + flattenImage(stack2, s, overlay2.duplicate(), showAll); + } + } + setStack(stack2); + } + + /** Flattens Overlay 'overlay' on slice 'slice' of ImageStack 'stack'. + * Copied from OverlayCommands by Marcel Boeglin 2014.01.08. + */ + private void flattenImage(ImageStack stack, int slice, Overlay overlay, boolean showAll) { + ImageProcessor ips = stack.getProcessor(slice); + ImagePlus imp1 = new ImagePlus("temp", ips); + int w = imp1.getWidth(); + int h = imp1.getHeight(); + for (int i=0; i=size) { + Object[] tmp1 = new Object[size*2]; + System.arraycopy(stack, 0, tmp1, 0, size); + stack = tmp1; + String[] tmp2 = new String[size*2]; + System.arraycopy(label, 0, tmp2, 0, size); + label = tmp2; + } + stack[nSlices-1] = pixels; + this.label[nSlices-1] = sliceLabel; + if (this.bitDepth==0) + setBitDepth(pixels); + } + + private void setBitDepth(Object pixels) { + if (pixels==null) + return; + if (pixels instanceof byte[]) + this.bitDepth = 8; + else if (pixels instanceof short[]) + this.bitDepth = 16; + else if (pixels instanceof float[]) + this.bitDepth = 32; + else if (pixels instanceof int[]) + this.bitDepth = 24; + } + + /** + * @deprecated + * Short images are always unsigned. + */ + public void addUnsignedShortSlice(String sliceLabel, Object pixels) { + addSlice(sliceLabel, pixels); + } + + /** Adds the image in 'ip' to the end of the stack. */ + public void addSlice(ImageProcessor ip) { + addSlice(null, ip); + } + + /** Adds the image in 'ip' to the end of the stack, setting + the string 'sliceLabel' as the slice metadata. */ + public void addSlice(String sliceLabel, ImageProcessor ip) { + ip = convertType(ip); + if (ip.getWidth()!=this.width || ip.getHeight()!=this.height) { + if (this.width==0 && this.height==0) + init(ip.getWidth(), ip.getHeight()); + else { + ImageProcessor ip2 = ip.createProcessor(this.width,this.height); + ip2.insert(ip, 0, 0); + ip = ip2; + } + } + if (nSlices==0) { + cm = ip.getColorModel(); + min = ip.getMin(); + max = ip.getMax(); + } + addSlice(sliceLabel, ip.getPixels()); + } + + private void init(int width, int height) { + this.width = width; + this.height = height; + stack = new Object[INITIAL_SIZE]; + label = new String[INITIAL_SIZE]; + } + + private ImageProcessor convertType(ImageProcessor ip) { + int newBitDepth = ip.getBitDepth(); + if (this.bitDepth==0) + this.bitDepth = newBitDepth; + if (this.bitDepth!=newBitDepth) { + switch (this.bitDepth) { + case 8: ip=ip.convertToByte(true); break; + case 16: ip=ip.convertToShort(true); break; + case 24: ip=ip.convertToRGB(); break; + case 32: ip=ip.convertToFloat(); break; + } + } + return ip; + } + + /** Adds the image in 'ip' to the stack following slice 'n'. Adds + the slice to the beginning of the stack if 'n' is zero. */ + public void addSlice(String sliceLabel, ImageProcessor ip, int n) { + if (n<0 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + addSlice(sliceLabel, ip); + Object tempSlice = stack[nSlices-1]; + String tempLabel = label[nSlices-1]; + int first = n>0?n:1; + for (int i=nSlices-1; i>=first; i--) { + stack[i] = stack[i-1]; + label[i] = label[i-1]; + } + stack[n] = tempSlice; + label[n] = tempLabel; + } + + /** Deletes the specified slice, were 1<=n<=nslices. */ + public void deleteSlice(int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + if (nSlices<1) + return; + for (int i=n; i0) + deleteSlice(nSlices); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public void setRoi(Rectangle roi) { + this.roi = roi; + } + + public Rectangle getRoi() { + if (roi==null) + return new Rectangle(0, 0, width, height); + else + return(roi); + } + + /** Updates this stack so its attributes, such as min, max, + calibration table and color model, are the same as 'ip'. */ + public void update(ImageProcessor ip) { + if (ip!=null) { + min = ip.getMin(); + max = ip.getMax(); + cTable = ip.getCalibrationTable(); + cm = ip.getColorModel(); + } + } + + /** Returns the pixel array for the specified slice, were 1<=n<=nslices. */ + public Object getPixels(int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + return stack[n-1]; + } + + /** Assigns a pixel array to the specified slice, + were 1<=n<=nslices. */ + public void setPixels(Object pixels, int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + stack[n-1] = pixels; + if (this.bitDepth==0) + setBitDepth(pixels); + } + + /** Returns the stack as an array of 1D pixel arrays. Note + that the size of the returned array may be greater than + the number of slices currently in the stack, with + unused elements set to null. */ + public Object[] getImageArray() { + return stack; + } + + /** Returns the number of slices in this stack. */ + public int size() { + return getSize(); + } + + public int getSize() { + return nSlices; + } + + /** Returns the slice labels as an array of Strings. Note + that the size of the returned array may be greater than + the number of slices currently in the stack. Returns null + if the stack is empty or the label of the first slice is null. */ + public String[] getSliceLabels() { + if (nSlices==0) + return null; + else + return label; + } + + /** Returns the label of the specified slice, were 1<=n<=nslices. + Returns null if the slice does not have a label or 'n'; + is out of range. For DICOM and FITS stacks, labels may + contain header information. + */ + public String getSliceLabel(int n) { + if (n<1 || n>nSlices) + return null; + else + return label[n-1]; + } + + /** Returns a shortened version (up to the first 60 characters + * or first newline), with the extension removed, of the specified + * slice label, or null if the slice does not have a label. + */ + public String getShortSliceLabel(int n) { + return getShortSliceLabel(n, 60); + } + + /** Returns a shortened version (up to the first 'max' characters + * or first newline), with the extension removed, of the specified + * slice label, or null if the slice does not have a label. + */ + public String getShortSliceLabel(int n, int max) { + String shortLabel = getSliceLabel(n); + if (shortLabel==null) return null; + int newline = shortLabel.indexOf('\n'); + if (newline==0) return null; + if (newline>0) + shortLabel = shortLabel.substring(0, newline); + int len = shortLabel.length(); + if (len>4 && shortLabel.charAt(len-4)=='.' && !Character.isDigit(shortLabel.charAt(len-1))) + shortLabel = shortLabel.substring(0,len-4); + if (shortLabel.length()>max) + shortLabel = shortLabel.substring(0, max); + return shortLabel; + } + + /** Sets the label of the specified slice, were 1<=n<=nslices. */ + public void setSliceLabel(String label, int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + this.label[n-1] = label; + } + + /** Returns an ImageProcessor for the specified slice, + were 1<=n<=nslices. Returns null if the stack is empty. + */ + public ImageProcessor getProcessor(int n) { + ImageProcessor ip; + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + if (nSlices==0) + return null; + if (stack[n-1]==null) + throw new IllegalArgumentException("Pixel array is null"); + if (stack[n-1] instanceof byte[]) + ip = new ByteProcessor(width, height, null, cm); + else if (stack[n-1] instanceof short[]) + ip = new ShortProcessor(width, height, null, cm); + else if (stack[n-1] instanceof int[]) { + if (signedInt) + ip = new IntProcessor(width, height); + else + ip = new ColorProcessor(width, height, null); + } else if (stack[n-1] instanceof float[]) + ip = new FloatProcessor(width, height, null, cm); + else + throw new IllegalArgumentException("Unknown stack type"); + ip.setPixels(stack[n-1]); + if (min!=Double.MAX_VALUE && ip!=null && !(ip instanceof ColorProcessor)) + ip.setMinAndMax(min, max); + if (cTable!=null) + ip.setCalibrationTable(cTable); + return ip; + } + + /** Assigns the pixel array of an ImageProcessor to the + specified slice, were 1<=n<=nslices. */ + public void setProcessor(ImageProcessor ip, int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + ip = convertType(ip); + if (ip.getWidth()!=width || ip.getHeight()!=height) + throw new IllegalArgumentException("Wrong dimensions for this stack"); + stack[n-1] = ip.getPixels(); + } + + /** Assigns a new color model to this stack. */ + public void setColorModel(ColorModel cm) { + this.cm = cm; + } + + /** Returns this stack's color model. May return null. */ + public ColorModel getColorModel() { + return cm; + } + + /** Returns true if this is a 3-slice, 8-bit RGB stack. */ + public boolean isRGB() { + return nSlices==3 && (stack[0] instanceof byte[]) && getSliceLabel(1)!=null && getSliceLabel(1).equals("Red"); + } + + /** Returns true if this is a 3-slice HSB stack. */ + public boolean isHSB() { + return nSlices==3 && bitDepth==8 && getSliceLabel(1)!=null && getSliceLabel(1).equals("Hue"); + } + + /** Returns true if this is a 3-slice 32-bit HSB stack. */ + public boolean isHSB32() { + return nSlices==3 && bitDepth==32 && getSliceLabel(1)!=null && getSliceLabel(1).equals("Hue"); + } + + /** Returns true if this is a Lab stack. */ + public boolean isLab() { + return nSlices==3 && getSliceLabel(1)!=null && getSliceLabel(1).equals("L*"); + } + + /** Returns true if this is a virtual (disk resident) stack. + This method is overridden by the VirtualStack subclass. */ + public boolean isVirtual() { + return false; + } + + /** Frees memory by deleting a few slices from the end of the stack. */ + public void trim() { + int n = (int)Math.round(Math.log(nSlices)+1.0); + for (int i=0; i=0 && x=0 && y=0 && z=0 && x=0 && y=0 && z255.0) + value = 255.0; + else if (value<0.0) + value = 0.0; + bytes[y*width+x] = (byte)(value+0.5); + break; + case 16: + short[] shorts = (short[])stack[z]; + if (value>65535.0) + value = 65535.0; + else if (value<0.0) + value = 0.0; + shorts[y*width+x] = (short)(value+0.5); + break; + case 32: + float[] floats = (float[])stack[z]; + floats[y*width+x] = (float)value; + break; + case 24: + int[] ints = (int[])stack[z]; + ints[y*width+x] = (int)value; + break; + } + } + } + + public float[] getVoxels(int x0, int y0, int z0, int w, int h, int d, float[] voxels) { + boolean inBounds = x0>=0 && x0+w<=width && y0>=0 && y0+h<=height && z0>=0 && z0+d<=nSlices; + if (voxels==null || voxels.length!=w*h*d) + voxels = new float[w*h*d]; + int i = 0; + int offset; + for (int z=z0; z=0 && x0+w<=width && y0>=0 && y0+h<=height && z0>=0 && z0+d<=nSlices; + if (voxels==null || voxels.length!=w*h*d) + voxels = new float[w*h*d]; + int i = 0; + for (int z=z0; z>16; break; + case 1: value=(value&0xff00)>>8; break; + case 2: value=value&0xff;; break; + } + voxels[i++] = (float)value; + } + } + } + return voxels; + } + + /** Experimental */ + public void setVoxels(int x0, int y0, int z0, int w, int h, int d, float[] voxels) { + boolean inBounds = x0>=0 && x0+w<=width && y0>=0 && y0+h<=height && z0>=0 && z0+d<=nSlices; + if (voxels==null || voxels.length!=w*h*d) + ; + int i = 0; + float value; + for (int z=z0; z255f) + value = 255f; + else if (value<0f) + value = 0f; + bytes[y*width+x] = (byte)(value+0.5f); + } + break; + case 16: + short[] shorts = (short[])stack[z]; + for (int x=x0; x65535f) + value = 65535f; + else if (value<0f) + value = 0f; + shorts[y*width+x] = (short)(value+0.5f); + } + break; + case 32: + float[] floats = (float[])stack[z]; + for (int x=x0; x=0 && x0+w<=width && y0>=0 && y0+h<=height && z0>=0 && z0+d<=nSlices; + if (voxels==null || voxels.length!=w*h*d) + ; + int i = 0; + for (int z=z0; z0) + setBitDepth(stack[0]); + return this.bitDepth; + } + + /** Sets the bit depth (8=byte, 16=short, 24=RGB, 32=float). */ + public void setBitDepth(int depth) { + if (size()==0 && (depth==8||depth==16||depth==24||depth==32)) + this.bitDepth = depth; + } + + /** Creates a new ImageStack. + * @param width width in pixels + * @param height height in pixels + * @param depth number of images + * @param bitdepth 8, 16, 32 (float) or 24 (RGB) + */ + public static ImageStack create(int width, int height, int depth, int bitdepth) { + ImageStack stack = IJ.createImage("", width, height, depth, bitdepth).getStack(); + if (bitdepth==16 || bitdepth==32) { + stack.min = Double.MAX_VALUE; + stack.max = 0.0; + } + return stack; + } + + /** Creates an ImageStack from an ImagePlus array. */ + public static ImageStack create(ImagePlus[] images) { + int w = 0; + int h = 0; + for (int i=0; iw) w=images[i].getWidth(); + if (images[i].getHeight()>h) h=images[i].getHeight(); + } + ImageStack stack = new ImageStack(w, h); + stack.init(w, h); + for (int i=0; ithis.width||y+height>this.height||z+depth>size()) + throw new IllegalArgumentException("Argument out of range"); + ImageStack stack2 = new ImageStack(width, height, getColorModel()); + for (int i=z; itrue if this is a 256 entry grayscale LUT. + @see ij.process.ImageProcessor#isColorLut + */ + public boolean isGrayscale() { + boolean isGray = true; + + if (mapSize < 256) + return false; + for (int i=0; i1) + return fs.saveAsTiffStack(path); + else + return fs.saveAsTiff(path); + } + + public static String getName(String path) { + int i = path.lastIndexOf('/'); + if (i==-1) + i = path.lastIndexOf('\\'); + if (i>0) + return path.substring(i+1); + else + return path; + } + + public static String getDir(String path) { + int i = path.lastIndexOf('/'); + if (i==-1) + i = path.lastIndexOf('\\'); + if (i>0) + return path.substring(0, i+1); + else + return ""; + } + + /** Aborts the currently running macro or any plugin using IJ.run(). */ + public static void abort() { + //IJ.log("Abort: "+Thread.currentThread().getName()); + abort = true; + if (Thread.currentThread().getName().endsWith("Macro$")) { + table.remove(Thread.currentThread()); + throw new RuntimeException(MACRO_CANCELED); + } + } + + /** If a command started using run(name, options) is running, + and the current thread is the same thread, + returns the options string, otherwise, returns null. + @see ij.gui.GenericDialog + @see ij.io.OpenDialog + */ + public static String getOptions() { + String threadName = Thread.currentThread().getName(); + //IJ.log("getOptions: "+threadName+" "+Thread.currentThread().hashCode()); //ts + if (threadName.startsWith("Run$_")||threadName.startsWith("RMI TCP")) { + Object options = table.get(Thread.currentThread()); + return options==null?null:options+" "; + } else + return null; + } + + /** Define a set of Macro options for the current Thread. */ + public static void setOptions(String options) { + //IJ.log("setOptions: "+Thread.currentThread().getName()+" "+Thread.currentThread().hashCode()+" "+options); //ts + if (options==null || options.equals("")) + table.remove(Thread.currentThread()); + else + table.put(Thread.currentThread(), options); + } + + /** Define a set of Macro options for a Thread. */ + public static void setOptions(Thread thread, String options) { + if (null==thread) + throw new RuntimeException("Need a non-null thread instance"); + if (null==options) + table.remove(thread); + else + table.put(thread, options); + } + + public static String getValue(String options, String key, String defaultValue) { + key = trimKey(key); + if (!options.endsWith(" ")) + options = options + " "; + key += '='; + int index=-1; + do { // Require that key not be preceded by a letter + index = options.indexOf(key, ++index); + if (index<0) return defaultValue; + } while (index!=0&&Character.isLetter(options.charAt(index-1))); + options = options.substring(index+key.length(), options.length()); + if (options.charAt(0)=='\'') { + index = options.indexOf("'",1); + if (index<0) + return defaultValue; + else + return options.substring(1, index); + } else if (options.charAt(0)=='[') { + int count = 1; + index = -1; + for (int i=1; i-1) + key = key.substring(0,index); + index = key.indexOf(":"); + if (index>-1) + key = key.substring(0,index); + key = key.toLowerCase(Locale.US); + return key; + } + + /** Evaluates 'code' and returns the output, or any error, + * as a String (e.g., Macro.eval("2+2") returns "4"). + */ + public static String eval(String code) { + return new Interpreter().eval(code); + } + +} + diff --git a/src/ij/Menus.java b/src/ij/Menus.java new file mode 100644 index 0000000..a988703 --- /dev/null +++ b/src/ij/Menus.java @@ -0,0 +1,1720 @@ +package ij; +import ij.process.*; +import ij.util.*; +import ij.gui.ImageWindow; +import ij.plugin.MacroInstaller; +import ij.gui.Toolbar; +import ij.macro.Interpreter; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; +import java.util.*; +import java.io.*; +import java.applet.Applet; +import java.awt.event.*; +import java.util.zip.*; + +/** +This class installs and updates ImageJ's menus. Note that menu labels, +even in submenus, must be unique. This is because ImageJ uses a single +hash table for all menu labels. If you look closely, you will see that +File->Import->Text Image... and File->Save As->Text Image... do not use +the same label. One of the labels has an extra space. + +@see ImageJ +*/ + +public class Menus { + + public static final char PLUGINS_MENU = 'p'; + public static final char IMPORT_MENU = 'i'; + public static final char SAVE_AS_MENU = 's'; + public static final char SHORTCUTS_MENU = 'h'; // 'h'=hotkey + public static final char ABOUT_MENU = 'a'; + public static final char FILTERS_MENU = 'f'; + public static final char TOOLS_MENU = 't'; + public static final char UTILITIES_MENU = 'u'; + + public static final int WINDOW_MENU_ITEMS = 6; // fixed items at top of Window menu + + public static final int NORMAL_RETURN = 0; + public static final int COMMAND_IN_USE = -1; + public static final int INVALID_SHORTCUT = -2; + public static final int SHORTCUT_IN_USE = -3; + public static final int NOT_INSTALLED = -4; + public static final int COMMAND_NOT_FOUND = -5; + + public static final int MAX_OPEN_RECENT_ITEMS = 15; + + private static Menus instance; + private static MenuBar mbar; + private static CheckboxMenuItem gray8Item,gray16Item,gray32Item, + color256Item,colorRGBItem,RGBStackItem,HSBStackItem,LabStackItem,HSB32Item; + private static PopupMenu popup; + + private static ImageJ ij; + private static Applet applet; + private Hashtable demoImagesTable = new Hashtable(); + private static String ImageJPath, pluginsPath, macrosPath; + private static Properties menus; + private static Properties menuSeparators; + private static Menu pluginsMenu, saveAsMenu, shortcutsMenu, utilitiesMenu, macrosMenu; + static Menu window, openRecentMenu; + private static Hashtable pluginsTable; + + private static int nPlugins, nMacros; + private static Hashtable shortcuts; + private static Hashtable macroShortcuts; + private static Vector pluginsPrefs; // commands saved in IJ_Prefs + static int windowMenuItems2; // non-image windows listed in Window menu + separator + private String error; + private String jarError; + private String pluginError; + private boolean isJarErrorHeading; + private static boolean installingJars, duplicateCommand; + private static Vector jarFiles; // JAR files in plugins folder with "_" in their name + private Map menuEntry2jarFile = new HashMap(); + private static Vector macroFiles; // Macros and scripts in the plugins folder + private static int userPluginsIndex; // First user plugin or submenu in Plugins menu + private static boolean addSorted; + private static int defaultFontSize = IJ.isWindows()?15:0; + private static int fontSize = Prefs.getInt(Prefs.MENU_SIZE, defaultFontSize); + private static Font menuFont; + private static double scale = 1.0; + + static boolean jnlp; // true when using Java WebStart + public static int setMenuBarCount; + + Menus(ImageJ ijInstance, Applet appletInstance) { + ij = ijInstance; + String title = ij!=null?ij.getTitle():null; + applet = appletInstance; + instance = this; + } + + String addMenuBar() { + scale = Prefs.getGuiScale(); + if ((scale>=1.5&&scale<2.0) || (scale>=2.5&&scale<3.0)) + scale = (int)Math.round(scale); + nPlugins = nMacros = userPluginsIndex = 0; + addSorted = installingJars = duplicateCommand = false; + error = null; + mbar = null; + menus = new Properties(); + pluginsTable = new Hashtable(); + shortcuts = new Hashtable(); + pluginsPrefs = new Vector(); + macroShortcuts = null; + setupPluginsAndMacrosPaths(); + Menu file = getMenu("File"); + Menu newMenu = getMenu("File>New", true); + addPlugInItem(file, "Open...", "ij.plugin.Commands(\"open\")", KeyEvent.VK_O, false); + addPlugInItem(file, "Open Next", "ij.plugin.NextImageOpener", KeyEvent.VK_O, true); + Menu openSamples = getMenu("File>Open Samples", true); + openSamples.addSeparator(); + addPlugInItem(openSamples, "Cache Sample Images ", "ij.plugin.URLOpener(\"cache\")", 0, false); + addOpenRecentSubMenu(file); + Menu importMenu = getMenu("File>Import", true); + Menu showFolderMenu = new Menu("Show Folder"); + file.add(showFolderMenu); + addPlugInItem(showFolderMenu, "Image", "ij.plugin.SimpleCommands(\"showdirImage\")", 0, false); + addPlugInItem(showFolderMenu, "Plugins", "ij.plugin.SimpleCommands(\"showdirPlugins\")", 0, false); + addPlugInItem(showFolderMenu, "Macros", "ij.plugin.SimpleCommands(\"showdirMacros\")", 0, false); + addPlugInItem(showFolderMenu, "LUTs", "ij.plugin.SimpleCommands(\"showdirLuts\")", 0, false); + addPlugInItem(showFolderMenu, "ImageJ", "ij.plugin.SimpleCommands(\"showdirImageJ\")", 0, false); + addPlugInItem(showFolderMenu, "temp", "ij.plugin.SimpleCommands(\"showdirTemp\")", 0, false); + addPlugInItem(showFolderMenu, "Home", "ij.plugin.SimpleCommands(\"showdirHome\")", 0, false); + file.addSeparator(); + addPlugInItem(file, "Close", "ij.plugin.Commands(\"close\")", KeyEvent.VK_W, false); + addPlugInItem(file, "Close All", "ij.plugin.Commands(\"close-all\")", KeyEvent.VK_W, true); + addPlugInItem(file, "Save", "ij.plugin.Commands(\"save\")", KeyEvent.VK_S, false); + saveAsMenu = getMenu("File>Save As", true); + addPlugInItem(file, "Revert", "ij.plugin.Commands(\"revert\")", KeyEvent.VK_R, true); + file.addSeparator(); + addPlugInItem(file, "Page Setup...", "ij.plugin.filter.Printer(\"setup\")", 0, false); + addPlugInItem(file, "Print...", "ij.plugin.filter.Printer(\"print\")", KeyEvent.VK_P, false); + + Menu edit = getMenu("Edit"); + addPlugInItem(edit, "Undo", "ij.plugin.Commands(\"undo\")", KeyEvent.VK_Z, false); + edit.addSeparator(); + addPlugInItem(edit, "Cut", "ij.plugin.Clipboard(\"cut\")", KeyEvent.VK_X, false); + addPlugInItem(edit, "Copy", "ij.plugin.Clipboard(\"copy\")", KeyEvent.VK_C, false); + addPlugInItem(edit, "Copy to System", "ij.plugin.Clipboard(\"scopy\")", 0, false); + addPlugInItem(edit, "Paste", "ij.plugin.Clipboard(\"paste\")", KeyEvent.VK_V, false); + addPlugInItem(edit, "Paste Control...", "ij.plugin.frame.PasteController", 0, false); + edit.addSeparator(); + addPlugInItem(edit, "Clear", "ij.plugin.filter.Filler(\"clear\")", 0, false); + addPlugInItem(edit, "Clear Outside", "ij.plugin.filter.Filler(\"outside\")", 0, false); + addPlugInItem(edit, "Fill", "ij.plugin.filter.Filler(\"fill\")", KeyEvent.VK_F, false); + addPlugInItem(edit, "Draw", "ij.plugin.filter.Filler(\"draw\")", KeyEvent.VK_D, false); + addPlugInItem(edit, "Invert", "ij.plugin.filter.Filters(\"invert\")", KeyEvent.VK_I, true); + edit.addSeparator(); + getMenu("Edit>Selection", true); + Menu optionsMenu = getMenu("Edit>Options", true); + + Menu image = getMenu("Image"); + Menu imageType = getMenu("Image>Type"); + gray8Item = addCheckboxItem(imageType, "8-bit", "ij.plugin.Converter(\"8-bit\")"); + gray16Item = addCheckboxItem(imageType, "16-bit", "ij.plugin.Converter(\"16-bit\")"); + gray32Item = addCheckboxItem(imageType, "32-bit", "ij.plugin.Converter(\"32-bit\")"); + color256Item = addCheckboxItem(imageType, "8-bit Color", "ij.plugin.Converter(\"8-bit Color\")"); + colorRGBItem = addCheckboxItem(imageType, "RGB Color", "ij.plugin.Converter(\"RGB Color\")"); + imageType.add(new MenuItem("-")); + RGBStackItem = addCheckboxItem(imageType, "RGB Stack", "ij.plugin.Converter(\"RGB Stack\")"); + HSBStackItem = addCheckboxItem(imageType, "HSB Stack", "ij.plugin.Converter(\"HSB Stack\")"); + HSB32Item = addCheckboxItem(imageType, "HSB (32-bit)", "ij.plugin.Converter(\"HSB (32-bit)\")"); + LabStackItem = addCheckboxItem(imageType, "Lab Stack", "ij.plugin.Converter(\"Lab Stack\")"); + image.add(imageType); + + image.addSeparator(); + getMenu("Image>Adjust", true); + addPlugInItem(image, "Show Info...", "ij.plugin.ImageInfo", KeyEvent.VK_I, false); + addPlugInItem(image, "Properties...", "ij.plugin.filter.ImageProperties", KeyEvent.VK_P, true); + getMenu("Image>Color", true); + getMenu("Image>Stacks", true); + getMenu("Image>Stacks>Animation_", true); + getMenu("Image>Stacks>Tools_", true); + Menu hyperstacksMenu = getMenu("Image>Hyperstacks", true); + image.addSeparator(); + addPlugInItem(image, "Crop", "ij.plugin.Resizer(\"crop\")", KeyEvent.VK_X, true); + addPlugInItem(image, "Duplicate...", "ij.plugin.Duplicator", KeyEvent.VK_D, true); + addPlugInItem(image, "Rename...", "ij.plugin.SimpleCommands(\"rename\")", 0, false); + addPlugInItem(image, "Scale...", "ij.plugin.Scaler", KeyEvent.VK_E, false); + getMenu("Image>Transform", true); + getMenu("Image>Zoom", true); + getMenu("Image>Overlay", true); + image.addSeparator(); + getMenu("Image>Lookup Tables", true); + + Menu process = getMenu("Process"); + addPlugInItem(process, "Smooth", "ij.plugin.filter.Filters(\"smooth\")", KeyEvent.VK_S, true); + addPlugInItem(process, "Sharpen", "ij.plugin.filter.Filters(\"sharpen\")", 0, false); + addPlugInItem(process, "Find Edges", "ij.plugin.filter.Filters(\"edge\")", 0, false); + addPlugInItem(process, "Find Maxima...", "ij.plugin.filter.MaximumFinder", 0, false); + addPlugInItem(process, "Enhance Contrast...", "ij.plugin.ContrastEnhancer", 0, false); + getMenu("Process>Noise", true); + getMenu("Process>Shadows", true); + getMenu("Process>Binary", true); + getMenu("Process>Math", true); + getMenu("Process>FFT", true); + Menu filtersMenu = getMenu("Process>Filters", true); + process.addSeparator(); + getMenu("Process>Batch", true); + addPlugInItem(process, "Image Calculator...", "ij.plugin.ImageCalculator", 0, false); + addPlugInItem(process, "Subtract Background...", "ij.plugin.filter.BackgroundSubtracter", 0, false); + addItem(process, "Repeat Command", KeyEvent.VK_R, false); + + Menu analyzeMenu = getMenu("Analyze"); + addPlugInItem(analyzeMenu, "Measure", "ij.plugin.filter.Analyzer", KeyEvent.VK_M, false); + addPlugInItem(analyzeMenu, "Analyze Particles...", "ij.plugin.filter.ParticleAnalyzer", 0, false); + addPlugInItem(analyzeMenu, "Summarize", "ij.plugin.filter.Analyzer(\"sum\")", 0, false); + addPlugInItem(analyzeMenu, "Distribution...", "ij.plugin.Distribution", 0, false); + addPlugInItem(analyzeMenu, "Label", "ij.plugin.filter.Filler(\"label\")", 0, false); + addPlugInItem(analyzeMenu, "Clear Results", "ij.plugin.filter.Analyzer(\"clear\")", 0, false); + addPlugInItem(analyzeMenu, "Set Measurements...", "ij.plugin.filter.Analyzer(\"set\")", 0, false); + analyzeMenu.addSeparator(); + addPlugInItem(analyzeMenu, "Set Scale...", "ij.plugin.filter.ScaleDialog", 0, false); + addPlugInItem(analyzeMenu, "Calibrate...", "ij.plugin.filter.Calibrator", 0, false); + if (IJ.isMacOSX()) { + addPlugInItem(analyzeMenu, "Histogram", "ij.plugin.Histogram", 0, false); + shortcuts.put(new Integer(KeyEvent.VK_H),"Histogram"); + } else + addPlugInItem(analyzeMenu, "Histogram", "ij.plugin.Histogram", KeyEvent.VK_H, false); + addPlugInItem(analyzeMenu, "Plot Profile", "ij.plugin.Profiler(\"plot\")", KeyEvent.VK_K, false); + addPlugInItem(analyzeMenu, "Surface Plot...", "ij.plugin.SurfacePlotter", 0, false); + getMenu("Analyze>Gels", true); + Menu toolsMenu = getMenu("Analyze>Tools", true); + + // the plugins will be added later, after a separator + addPluginsMenu(); + + Menu window = getMenu("Window"); + addPlugInItem(window, "Show All", "ij.plugin.WindowOrganizer(\"show\")", KeyEvent.VK_CLOSE_BRACKET, false); + String key = IJ.isWindows()?"enter":"return"; + addPlugInItem(window, "Main Window ["+key+"]", "ij.plugin.WindowOrganizer(\"imagej\")", 0, false); + addPlugInItem(window, "Put Behind [tab]", "ij.plugin.Commands(\"tab\")", 0, false); + addPlugInItem(window, "Cascade", "ij.plugin.WindowOrganizer(\"cascade\")", 0, false); + addPlugInItem(window, "Tile", "ij.plugin.WindowOrganizer(\"tile\")", 0, false); + window.addSeparator(); + + Menu help = getMenu("Help"); + addPlugInItem(help, "ImageJ Website...", "ij.plugin.BrowserLauncher", 0, false); + help.addSeparator(); + addPlugInItem(help, "Dev. Resources...", "ij.plugin.BrowserLauncher(\""+IJ.URL+"/developer/index.html\")", 0, false); + addPlugInItem(help, "Plugins...", "ij.plugin.BrowserLauncher(\""+IJ.URL+"/plugins\")", 0, false); + addPlugInItem(help, "Macros...", "ij.plugin.BrowserLauncher(\""+IJ.URL+"/macros/\")", 0, false); + addPlugInItem(help, "Macro Functions...", "ij.plugin.BrowserLauncher(\""+IJ.URL+"/developer/macro/functions.html\")", 0, false); + Menu examplesMenu = getExamplesMenu(ij); + addPlugInItem(examplesMenu, "Open as Panel", "ij.plugin.SimpleCommands(\"opencp\")", 0, false); + help.add(examplesMenu); + help.addSeparator(); + addPlugInItem(help, "Update ImageJ...", "ij.plugin.ImageJ_Updater", 0, false); + addPlugInItem(help, "Refresh Menus", "ij.plugin.ImageJ_Updater(\"menus\")", 0, false); + help.addSeparator(); + Menu aboutMenu = getMenu("Help>About Plugins", true); + addPlugInItem(help, "About ImageJ...", "ij.plugin.AboutBox", 0, false); + + if (applet==null) { + menuSeparators = new Properties(); + installPlugins(); + } + + // make sure "Quit" is the last item in the File menu + file.addSeparator(); + addPlugInItem(file, "Quit", "ij.plugin.Commands(\"quit\")", 0, false); + + //System.out.println("MenuBar.setFont: "+fontSize+" "+scale+" "+getFont()); + if (fontSize!=0 || scale>1.0) + mbar.setFont(getFont()); + if (ij!=null) { + ij.setMenuBar(mbar); + Menus.setMenuBarCount++; + } + + // Add deleted sample images to commands table + pluginsTable.put("Lena (68K)", "ij.plugin.URLOpener(\"lena-std.tif\")"); + pluginsTable.put("Bridge (174K)", "ij.plugin.URLOpener(\"bridge.gif\")"); + + if (pluginError!=null) + error = error!=null?error+="\n"+pluginError:pluginError; + if (jarError!=null) + error = error!=null?error+="\n"+jarError:jarError; + return error; + } + + public static Menu getExamplesMenu(ActionListener listener) { + Menu menu = new Menu("Examples"); + Menu submenu = new Menu("Plots"); + addExample(submenu, "Example Plot", "Example_Plot_.ijm"); + addExample(submenu, "Semi-log Plot", "Semi-log_Plot_.ijm"); + addExample(submenu, "Arrow Plot", "Arrow_Plot_.ijm"); + addExample(submenu, "Damped Wave Plot", "Damped_Wave_Plot_.ijm"); + addExample(submenu, "Dynamic Plot", "Dynamic_Plot_.ijm"); + addExample(submenu, "Dynamic Plot 2D", "Dynamic_Plot_2D_.ijm"); + addExample(submenu, "Custom Plot Symbols", "Custom_Plot_Symbols_.ijm"); + addExample(submenu, "Histograms", "Histograms_.ijm"); + addExample(submenu, "Bar Charts", "Bar_Charts_.ijm"); + addExample(submenu, "Shapes", "Plot_Shapes_.ijm"); + addExample(submenu, "Plot Styles", "Plot_Styles_.ijm"); + addExample(submenu, "Random Data", "Random_Data_.ijm"); + addExample(submenu, "Plot Results", "Plot_Results_.ijm"); + submenu.addActionListener(listener); + menu.add(submenu); + + submenu = new Menu("Tools"); + addExample(submenu, "Annular Selection", "Annular_Selection_Tool.ijm"); + addExample(submenu, "Big Cursor", "Big_Cursor_Tool.ijm"); + addExample(submenu, "Circle Tool", "Circle_Tool.ijm"); + addExample(submenu, "Point Picker", "Point_Picker_Tool.ijm"); + addExample(submenu, "Star Tool", "Star_Tool.ijm"); + addExample(submenu, "Animated Icon Tool", "Animated_Icon_Tool.ijm"); + submenu.addActionListener(listener); + menu.add(submenu); + + submenu = new Menu("Macro"); + addExample(submenu, "Sphere", "Sphere.ijm"); + addExample(submenu, "Dialog Box", "Dialog_Box.ijm"); + addExample(submenu, "Process Folder", "Batch_Process_Folder.ijm"); + addExample(submenu, "OpenDialog Demo", "OpenDialog_Demo.ijm"); + addExample(submenu, "Save All Images", "Save_All_Images.ijm"); + addExample(submenu, "Sine/Cosine Table", "Sine_Cosine_Table.ijm"); + addExample(submenu, "Non-numeric Table", "Non-numeric_Table.ijm"); + addExample(submenu, "Overlay", "Overlay.ijm"); + addExample(submenu, "Stack Overlay", "Stack_Overlay.ijm"); + addExample(submenu, "Array Functions", "Array_Functions.ijm"); + addExample(submenu, "Dual Progress Bars", "Dual_Progress_Bars.ijm"); + addExample(submenu, "Grab Viridis Colormap", "Grab_Viridis_Colormap.ijm"); + addExample(submenu, "Custom Measurement", "Custom_Measurement.ijm"); + addExample(submenu, "Synthetic Images", "Synthetic_Images.ijm"); + addExample(submenu, "Spiral Rotation", "Spiral_Rotation.ijm"); + addExample(submenu, "Curve Fitting", "Curve_Fitting.ijm"); + addExample(submenu, "Colors of 2021", "Colors_of_2021.ijm"); + submenu.addActionListener(listener); + menu.add(submenu); + + submenu = new Menu("JavaScript"); + addExample(submenu, "Sphere", "Sphere.js"); + addExample(submenu, "Plasma Cloud", "Plasma_Cloud.js"); + addExample(submenu, "Cloud Debugger", "Cloud_Debugger.js"); + addExample(submenu, "Synthetic Images", "Synthetic_Images.js"); + addExample(submenu, "Points", "Points.js"); + addExample(submenu, "Spiral Rotation", "Spiral_Rotation.js"); + addExample(submenu, "Example Plot", "Example_Plot.js"); + addExample(submenu, "Semi-log Plot", "Semi-log_Plot.js"); + addExample(submenu, "Arrow Plot", "Arrow_Plot.js"); + addExample(submenu, "Dynamic Plot", "Dynamic_Plot.js"); + addExample(submenu, "Plot Styles", "Plot_Styles.js"); + addExample(submenu, "Plot Random Data", "Plot_Random_Data.js"); + addExample(submenu, "Histogram Plots", "Histogram_Plots.js"); + addExample(submenu, "JPEG Quality Plot", "JPEG_Quality_Plot.js"); + addExample(submenu, "Process Folder", "Batch_Process_Folder.js"); + addExample(submenu, "Sine/Cosine Table", "Sine_Cosine_Table.js"); + addExample(submenu, "Non-numeric Table", "Non-numeric_Table.js"); + addExample(submenu, "Overlay", "Overlay.js"); + addExample(submenu, "Stack Overlay", "Stack_Overlay.js"); + addExample(submenu, "Dual Progress Bars", "Dual_Progress_Bars.js"); + addExample(submenu, "Gamma Adjuster", "Gamma_Adjuster.js"); + addExample(submenu, "Custom Measurement", "Custom_Measurement.js"); + addExample(submenu, "Terabyte VirtualStack", "Terabyte_VirtualStack.js"); + addExample(submenu, "Event Listener", "Event_Listener.js"); + addExample(submenu, "FFT Filter", "FFT_Filter.js"); + addExample(submenu, "Curve Fitting", "Curve_Fitting.js"); + addExample(submenu, "Overlay Text", "Overlay_Text.js"); + addExample(submenu, "Crop Multiple Rois", "Crop_Multiple_Rois.js"); + addExample(submenu, "Show all LUTs", "Show_all_LUTs.js"); + addExample(submenu, "Dialog Demo", "Dialog_Demo.js"); + submenu.addActionListener(listener); + menu.add(submenu); + submenu = new Menu("BeanShell"); + addExample(submenu, "Sphere", "Sphere.bsh"); + addExample(submenu, "Example Plot", "Example_Plot.bsh"); + addExample(submenu, "Semi-log Plot", "Semi-log_Plot.bsh"); + addExample(submenu, "Arrow Plot", "Arrow_Plot.bsh"); + addExample(submenu, "Sine/Cosine Table", "Sine_Cosine_Table.bsh"); + submenu.addActionListener(listener); + menu.add(submenu); + submenu = new Menu("Python"); + addExample(submenu, "Sphere", "Sphere.py"); + addExample(submenu, "Animated Gaussian Blur", "Animated_Gaussian_Blur.py"); + addExample(submenu, "Spiral Rotation", "Spiral_Rotation.py"); + addExample(submenu, "Overlay", "Overlay.py"); + submenu.addActionListener(listener); + menu.add(submenu); + submenu = new Menu("Java"); + addExample(submenu, "Sphere", "Sphere_.java"); + addExample(submenu, "Plasma Cloud", "Plasma_Cloud.java"); + addExample(submenu, "Gamma Adjuster", "Gamma_Adjuster.java"); + addExample(submenu, "Plugin", "My_Plugin.java"); + addExample(submenu, "Plugin Filter", "Filter_Plugin.java"); + addExample(submenu, "Plugin Frame", "Plugin_Frame.java"); + addExample(submenu, "Plugin Tool", "Prototype_Tool.java"); + submenu.addActionListener(listener); + menu.add(submenu); + menu.addSeparator(); + CheckboxMenuItem item = new CheckboxMenuItem("Autorun Examples"); + menu.add(item); + item.addItemListener(ij); + item.setState(Prefs.autoRunExamples); + return menu; + } + + private static void addExample(Menu menu, String label, String command) { + MenuItem item = new MenuItem(label); + menu.add(item); + item.setActionCommand(command); + } + + void addOpenRecentSubMenu(Menu menu) { + openRecentMenu = getMenu("File>Open Recent"); + for (int i=0; i0) + key = key.substring(0, index); + for (int count=1; count<100; count++) { + value = Prefs.getString(key + (count/10)%10 + count%10); + if (value==null) + break; + if (count==1) + menu.add(submenu); + if (value.equals("-")) + submenu.addSeparator(); + else + addPluginItem(submenu, value); + } + if (name.equals("Lookup Tables") && applet==null) + addLuts(submenu); + return submenu; + } + + static void addLuts(Menu submenu) { + String path = IJ.getDirectory("luts"); + if (path==null) return; + File f = new File(path); + String[] list = null; + if (applet==null && f.exists() && f.isDirectory()) + list = f.list(); + if (list==null) return; + if (IJ.isLinux() || IJ.isMacOSX()) + Arrays.sort(list); + submenu.addSeparator(); + for (int i=0; i0) { + String shortcut = command.substring(openBracket+1,command.length()-1); + keyCode = convertShortcutToCode(shortcut); + boolean functionKey = keyCode>=KeyEvent.VK_F1 && keyCode<=KeyEvent.VK_F12; + if (keyCode>0 && !functionKey) + command = command.substring(0,openBracket); + } + } + if (keyCode>=KeyEvent.VK_F1 && keyCode<=KeyEvent.VK_F12) { + shortcuts.put(new Integer(keyCode),command); + keyCode = 0; + } else if (keyCode>=265 && keyCode<=290) { + keyCode -= 200; + shift = true; + } + addItem(submenu,command,keyCode,shift); + while(s.charAt(lastComma+1)==' ' && lastComma+2') { + String submenu = value.substring(2,value.length()-1); + //Menu menu = getMenu("Plugins>" + submenu, true); + Menu menu = addSubMenu(pluginsMenu, submenu); + if (submenu.equals("Shortcuts")) + shortcutsMenu = menu; + else if (submenu.equals("Utilities")) + utilitiesMenu = menu; + else if (submenu.equals("Macros")) + macrosMenu = menu; + } else + addPluginItem(pluginsMenu, value); + } + userPluginsIndex = pluginsMenu.getItemCount(); + if (userPluginsIndex<0) userPluginsIndex = 0; + } + + /** Install plugins using "pluginxx=" keys in IJ_Prefs.txt. + Plugins not listed in IJ_Prefs are added to the end + of the Plugins menu. */ + void installPlugins() { + int nPlugins0 = nPlugins; + String value, className; + char menuCode; + Menu menu; + String[] pluginList = getPlugins(); + String[] pluginsList2 = null; + Hashtable skipList = new Hashtable(); + for (int index=0; index<100; index++) { + value = Prefs.getString("plugin" + (index/10)%10 + index%10); + if (value==null) + break; + menuCode = value.charAt(0); + switch (menuCode) { + case PLUGINS_MENU: default: menu = pluginsMenu; break; + case IMPORT_MENU: menu = getMenu("File>Import"); break; + case SAVE_AS_MENU: menu = getMenu("File>Save As"); break; + case SHORTCUTS_MENU: menu = shortcutsMenu; break; + case ABOUT_MENU: menu = getMenu("Help>About Plugins"); break; + case FILTERS_MENU: menu = getMenu("Process>Filters"); break; + case TOOLS_MENU: menu = getMenu("Analyze>Tools"); break; + case UTILITIES_MENU: menu = utilitiesMenu; break; + } + String prefsValue = value; + value = value.substring(2,value.length()); //remove menu code and coma + className = value.substring(value.lastIndexOf(',')+1,value.length()); + boolean found = className.startsWith("ij."); + if (!found && pluginList!=null) { // does this plugin exist? + if (pluginsList2==null) + pluginsList2 = getStrippedPlugins(pluginList); + for (int i=0; i0) + className = className.substring(0, argStart); + } + skipList.put(className, ""); + } + } + if (pluginList!=null) { + for (int i=0; i0) { + dir = name.substring(0, slashIndex); + name = name.substring(slashIndex+1, name.length()); + menu = getPluginsSubmenu(dir); + slashIndex = name.indexOf('/'); + if (slashIndex>0) { + String dir2 = name.substring(0, slashIndex); + name = name.substring(slashIndex+1, name.length()); + String menuName = "Plugins>"+dir+">"+dir2; + menu = getMenu(menuName); + dir += File.separator+dir2; + } + } + String command = name.replace('_',' '); + if (command.endsWith(".js")||command.endsWith(".py")) + command = command.substring(0, command.length()-3); //remove ".js" or ".py" + else + command = command.substring(0, command.length()-4); //remove ".txt", ".ijm" or ".bsh" + command.trim(); + if (pluginsTable.get(command)!=null) // duplicate command? + command = command + " Macro"; + MenuItem item = new MenuItem(command); + addOrdered(menu, item); + item.addActionListener(ij); + String path = (dir!=null?dir+File.separator:"") + name; + pluginsTable.put(command, "ij.plugin.Macro_Runner(\""+path+"\")"); + nMacros++; + } + + static int addPluginSeparatorIfNeeded(Menu menu) { + if (menuSeparators == null) + return 0; + Integer i = (Integer)menuSeparators.get(menu); + if (i == null) { + if (menu.getItemCount() > 0) + addSeparator(menu); + i = new Integer(menu.getItemCount()); + menuSeparators.put(menu, i); + } + return i.intValue(); + } + + /** Inserts 'item' into 'menu' in alphanumeric order. */ + static void addOrdered(Menu menu, MenuItem item) { + String label = item.getLabel(); + int start = addPluginSeparatorIfNeeded(menu); + for (int i=start; i=3 && !s.startsWith("#")) + entries.add(s); + } + } + catch (IOException e) {} + finally { + try {if (lnr!=null) lnr.close();} + catch (IOException e) {} + } + for (int j=0; j")) { + int firstComma = s.indexOf(','); + if (firstComma==-1 || firstComma<=8) + menu = null; + else { + String name = s.substring(8, firstComma); + menu = getPluginsSubmenu(name); + } + } else if (s.startsWith("\"") || s.startsWith("Plugins")) { + String name = getSubmenuName(jar); + if (name!=null) + menu = getPluginsSubmenu(name); + else + menu = pluginsMenu; + addSorted = true; + } else { + int firstQuote = s.indexOf('"'); + String name = firstQuote<0 ? s : s.substring(0, firstQuote).trim(); + int comma = name.indexOf(','); + if (comma >= 0) + name = name.substring(0, comma); + if (name.startsWith("Help>About")) // for backward compatibility + name = "Help>About Plugins"; + menu = getMenu(name); + } + int firstQuote = s.indexOf('"'); + if (firstQuote==-1) + return; + s = s.substring(firstQuote, s.length()); // remove menu + if (menu!=null) { + addPluginSeparatorIfNeeded(menu); + addPluginItem(menu, s); + addSorted = false; + } + String menuEntry = s; + if (s.startsWith("\"")) { + int quote = s.indexOf('"', 1); + menuEntry = quote<0?s.substring(1):s.substring(1, quote); + } else { + int comma = s.indexOf(','); + if (comma > 0) + menuEntry = s.substring(0, comma); + } + if (duplicateCommand) { + if (jarError==null) jarError = ""; + addJarErrorHeading(jar); + String jar2 = (String)menuEntry2jarFile.get(menuEntry); + if (jar2 != null && jar2.startsWith(pluginsPath)) + jar2 = jar2.substring(pluginsPath.length()); + jarError += " Duplicate command: " + s + + (jar2 != null ? " (already in " + jar2 + ")" + : "") + "\n"; + } else + menuEntry2jarFile.put(menuEntry, jar); + duplicateCommand = false; + } + + void addJarErrorHeading(String jar) { + if (!isJarErrorHeading) { + if (!jarError.equals("")) + jarError += " \n"; + jarError += "Plugin configuration error: " + jar + "\n"; + isJarErrorHeading = true; + } + } + + /** Returns the specified ImageJ menu (e.g., "File>New") or null if it is not found. */ + public static Menu getImageJMenu(String menuPath) { + if (menus==null) + IJ.init(); + if (menus==null) + return null; + if (menus.get(menuPath)!=null) + return getMenu(menuPath, false); + else + return null; + } + + private static Menu getMenu(String menuPath) { + return getMenu(menuPath, false); + } + + private static Menu getMenu(String menuName, boolean readFromProps) { + if (menuName.endsWith(">")) + menuName = menuName.substring(0, menuName.length() - 1); + Menu result = (Menu)menus.get(menuName); + if (result==null) { + int offset = menuName.lastIndexOf('>'); + if (offset < 0) { + result = new Menu(menuName); + if (mbar == null) + mbar = new MenuBar(); + if (menuName.equals("Help")) + mbar.setHelpMenu(result); + else + mbar.add(result); + if (menuName.equals("Window")) + window = result; + else if (menuName.equals("Plugins")) + pluginsMenu = result; + } else { + String parentName = menuName.substring(0, offset); + String menuItemName = menuName.substring(offset + 1); + Menu parentMenu = getMenu(parentName); + result = new Menu(menuItemName); + addPluginSeparatorIfNeeded(parentMenu); + if (readFromProps) + result = addSubMenu(parentMenu, menuItemName); + else if (parentName.startsWith("Plugins") && menuSeparators != null) + addItemSorted(parentMenu, result, parentName.equals("Plugins")?userPluginsIndex:0); + else + parentMenu.add(result); + if (menuName.equals("File>Open Recent")) + openRecentMenu = result; + } + menus.put(menuName, result); + } + return result; + } + + Menu getPluginsSubmenu(String submenuName) { + return getMenu("Plugins>" + submenuName); + } + + String getSubmenuName(String jarPath) { + //IJ.log("getSubmenuName: \n"+jarPath+"\n"+pluginsPath); + if (pluginsPath == null) + return null; + if (jarPath.startsWith(pluginsPath)) + jarPath = jarPath.substring(pluginsPath.length() - 1); + int index = jarPath.lastIndexOf(File.separatorChar); + if (index<0) return null; + String name = jarPath.substring(0, index); + index = name.lastIndexOf(File.separatorChar); + if (index<0) return null; + name = name.substring(index+1); + if (name.equals("plugins")) return null; + return name; + } + + static void addItemSorted(Menu menu, MenuItem item, int startingIndex) { + String itemLabel = item.getLabel(); + int count = menu.getItemCount(); + boolean inserted = false; + for (int i=startingIndex; i0 && name.indexOf("$")==-1 + && name.indexOf("/_")==-1 && !name.startsWith("_")) { + if (Character.isLowerCase(name.charAt(0))&&name.indexOf("/")!=-1) + continue; + if (sb==null) sb = new StringBuffer(); + String className = name.substring(0, name.length()-6); + int slashIndex = className.lastIndexOf('/'); + String plugins = "Plugins"; + if (slashIndex >= 0) { + plugins += ">" + className.substring(0, slashIndex).replace('/', '>').replace('_', ' '); + name = className.substring(slashIndex + 1); + } else + name = className; + name = name.replace('_', ' '); + className = className.replace('/', '.'); + //if (className.indexOf(".")==-1 || Character.isUpperCase(className.charAt(0))) + sb.append(plugins + ", \""+name+"\", "+className+"\n"); + } + } + } + catch (Throwable e) { + IJ.log(jar+": "+e); + } + //IJ.log(""+(sb!=null?sb.toString():"null")); + if (sb==null) + return null; + else + return new ByteArrayInputStream(sb.toString().getBytes()); + } + + /** Returns a list of the plugins with directory names removed. */ + String[] getStrippedPlugins(String[] plugins) { + String[] plugins2 = new String[plugins.length]; + int slashPos; + for (int i=0; i=0) + plugins2[i] = plugins[i].substring(slashPos+1,plugins2[i].length()); + } + return plugins2; + } + + void setupPluginsAndMacrosPaths() { + ImageJPath = pluginsPath = macrosPath = null; + String currentDir = Prefs.getHomeDir(); // "user.dir" + if (currentDir==null) + return; + if (currentDir.endsWith("plugins")) + ImageJPath = pluginsPath = currentDir+File.separator; + else { + String pluginsDir = System.getProperty("plugins.dir"); + if (pluginsDir!=null) { + if (pluginsDir.endsWith("/")||pluginsDir.endsWith("\\")) + pluginsDir = pluginsDir.substring(0, pluginsDir.length()-1); + if (pluginsDir.endsWith("/plugins")||pluginsDir.endsWith("\\plugins")) + pluginsDir = pluginsDir.substring(0, pluginsDir.length()-8); + } + if (pluginsDir==null) + pluginsDir = currentDir; + else if (pluginsDir.equals("user.home")) { + pluginsDir = System.getProperty("user.home"); + if (!(new File(pluginsDir+File.separator+"plugins")).isDirectory()) + pluginsDir = pluginsDir + File.separator + "ImageJ"; + // needed to run plugins when ImageJ launched using Java WebStart + if (applet==null) + System.setSecurityManager(null); + jnlp = true; + } + pluginsPath = pluginsDir+File.separator+"plugins"+File.separator; + macrosPath = pluginsDir+File.separator+"macros"+File.separator; + ImageJPath = pluginsDir+File.separator; + } + File f = pluginsPath!=null?new File(pluginsPath):null; + if (f==null || !f.isDirectory()) { + ImageJPath = currentDir+File.separator; + pluginsPath = ImageJPath+"plugins"+File.separator; + f = new File(pluginsPath); + if (!f.isDirectory()) { + String altPluginsPath = System.getProperty("plugins.dir"); + if (altPluginsPath!=null) { + f = new File(altPluginsPath); + if (!f.isDirectory()) + altPluginsPath = null; + else { + ImageJPath = f.getParent() + File.separator; + pluginsPath = ImageJPath + f.getName() + File.separator; + macrosPath = ImageJPath+"macros"+File.separator; + } + } + if (altPluginsPath==null) + ImageJPath = pluginsPath = null; + } + } + f = macrosPath!=null?new File(macrosPath):null; + if (f!=null && !f.isDirectory()) { + macrosPath = currentDir+File.separator+"macros"+File.separator; + f = new File(macrosPath); + if (!f.isDirectory()) + macrosPath = null; + } + if (IJ.debugMode) { + IJ.log("Menus.setupPluginsAndMacrosPaths"); + IJ.log(" user.dir: "+currentDir); + IJ.log(" plugins.dir: "+System.getProperty("plugins.dir")); + IJ.log(" ImageJPath: "+ImageJPath); + IJ.log(" pluginsPath: "+pluginsPath); + } + } + + /** Returns a list of the plugins in the plugins menu. */ + public static synchronized String[] getPlugins() { + File f = pluginsPath!=null?new File(pluginsPath):null; + if (f==null || (f!=null && !f.isDirectory())) + return null; + String[] list = f.list(); + if (list==null) + return null; + Vector v = new Vector(); + jarFiles = null; + macroFiles = null; + for (int i=0; i=0; + if (hasUnderscore && isClassFile && name.indexOf('$')<0 ) { + name = name.substring(0, name.length()-6); // remove ".class" + v.addElement(name); + } else if (hasUnderscore && (name.endsWith(".jar") || name.endsWith(".zip"))) { + if (jarFiles==null) jarFiles = new Vector(); + jarFiles.addElement(pluginsPath + name); + } else if (validMacroName(name,hasUnderscore)) { + if (macroFiles==null) macroFiles = new Vector(); + macroFiles.addElement(name); + } else { + if (!isClassFile) + checkSubdirectory(pluginsPath, name, v); + } + } + list = new String[v.size()]; + v.copyInto((String[])list); + StringSorter.sort(list); + return list; + } + + /** Looks for plugins and jar files in a subdirectory of the plugins directory. */ + private static void checkSubdirectory(String path, String dir, Vector v) { + if (dir.endsWith(".java")) + return; + File f = new File(path, dir); + if (!f.isDirectory()) + return; + String[] list = f.list(); + if (list==null) + return; + dir += "/"; + int classCount=0, otherCount=0; + String className = null; + for (int i=0; i=0; + if (hasUnderscore && name.endsWith(".class") && name.indexOf('$')<0) { + name = name.substring(0, name.length()-6); // remove ".class" + v.addElement(dir+name); + classCount++; + className = name; + } else if (hasUnderscore && (name.endsWith(".jar") || name.endsWith(".zip"))) { + if (jarFiles==null) jarFiles = new Vector(); + jarFiles.addElement(f.getPath() + File.separator + name); + otherCount++; + } else if (validMacroName(name,hasUnderscore)) { + if (macroFiles==null) macroFiles = new Vector(); + macroFiles.addElement(dir + name); + otherCount++; + } else { + File f2 = new File(f, name); + if (f2.isDirectory()) installSubdirectorMacros(f2, dir+name); + } + } + if (Prefs.moveToMisc && classCount==1 && otherCount==0 && dir.indexOf("_")==-1) + v.setElementAt("Miscellaneous/" + className, + v.size() - 1); + } + + /** Installs macros and scripts located in subdirectories. */ + private static void installSubdirectorMacros(File f2, String dir) { + if (dir.endsWith("Launchers")) return; + String[] list = f2.list(); + if (list==null) return; + for (int i=0; i=0; + if (validMacroName(name,hasUnderscore)) { + if (macroFiles==null) macroFiles = new Vector(); + macroFiles.addElement(dir+"/"+name); + } + } + } + + private static boolean validMacroName(String name, boolean hasUnderscore) { + return (hasUnderscore&&name.endsWith(".txt")) || name.endsWith(".ijm") + || name.endsWith(".js") || name.endsWith(".bsh") || name.endsWith(".py"); + } + + /** Installs a plugin in the Plugins menu using the class name, + with underscores replaced by spaces, as the command. */ + void installUserPlugin(String className) { + installUserPlugin(className, false); + } + + public void installUserPlugin(String className, boolean force) { + int slashIndex = className.indexOf('/'); + String menuName = slashIndex < 0 ? "Plugins" : "Plugins>" + + className.substring(0, slashIndex).replace('/', '>'); + Menu menu = getMenu(menuName); + String command = className; + if (slashIndex>0) { + command = className.substring(slashIndex+1); + } + command = command.replace('_',' '); + command.trim(); + boolean itemExists = (pluginsTable.get(command)!=null); + if(force && itemExists) + return; + + if (!force && itemExists) // duplicate command? + command = command + " Plugin"; + MenuItem item = new MenuItem(command); + if(force) + addItemSorted(menu,item,0); + else + addOrdered(menu, item); + item.addActionListener(ij); + pluginsTable.put(command, className.replace('/', '.')); + nPlugins++; + } + + void installPopupMenu(ImageJ ij) { + String s; + int count = 0; + MenuItem mi; + popup = new PopupMenu(""); + if (fontSize!=0 || scale>1.0) + popup.setFont(getFont()); + while (true) { + count++; + s = Prefs.getString("popup" + (count/10)%10 + count%10); + if (s==null) + break; + if (s.equals("-")) + popup.addSeparator(); + else if (!s.equals("")) { + mi = new MenuItem(s); + mi.addActionListener(ij); + popup.add(mi); + } + } + } + + public static MenuBar getMenuBar() { + return mbar; + } + + public static Menu getMacrosMenu() { + return macrosMenu; + } + + public static Menu getOpenRecentMenu() { + return openRecentMenu; + } + + public int getMacroCount() { + return nMacros; + } + + public int getPluginCount() { + return nPlugins; + } + + static final int RGB_STACK=10, HSB_STACK=11, LAB_STACK=12, HSB32_STACK=13; + + /** Updates the Image/Type and Window menus. */ + public static void updateMenus() { + if (ij==null) return; + gray8Item.setState(false); + gray16Item.setState(false); + gray32Item.setState(false); + color256Item.setState(false); + colorRGBItem.setState(false); + RGBStackItem.setState(false); + HSBStackItem.setState(false); + LabStackItem.setState(false); + HSB32Item.setState(false); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) + return; + int type = imp.getType(); + if (imp.getStackSize()>1) { + ImageStack stack = imp.getStack(); + if (stack.isRGB()) + type = RGB_STACK; + else if (stack.isHSB()) + type = HSB_STACK; + else if (stack.isLab()) + type = LAB_STACK; + else if (stack.isHSB32()) + type = HSB32_STACK; + } + switch (type) { + case ImagePlus.GRAY8: + gray8Item.setState(true); + break; + case ImagePlus.GRAY16: + gray16Item.setState(true); + break; + case ImagePlus.GRAY32: + gray32Item.setState(true); + break; + case ImagePlus.COLOR_256: + color256Item.setState(true); + break; + case ImagePlus.COLOR_RGB: + colorRGBItem.setState(true); + break; + case RGB_STACK: + RGBStackItem.setState(true); + break; + case HSB_STACK: + HSBStackItem.setState(true); + break; + case LAB_STACK: + LabStackItem.setState(true); + break; + case HSB32_STACK: + HSB32Item.setState(true); + break; + } + + //update Window menu + int nItems = window.getItemCount(); + int start = WINDOW_MENU_ITEMS + windowMenuItems2; + int index = start + WindowManager.getCurrentIndex(); + try { // workaround for Linux/Java 5.0/bug + for (int i=start; i=2) + index--; + window.insert(item, index); + windowMenuItems2++; + if (windowMenuItems2==1) { + window.insertSeparator(WINDOW_MENU_ITEMS+windowMenuItems2); + windowMenuItems2++; + } + } + + /** Adds one image to the end of the Window menu. */ + static synchronized void addWindowMenuItem(ImagePlus imp) { + if (ij==null) + return; + String name = imp.getTitle(); + String size = ImageWindow.getImageSize(imp); + CheckboxMenuItem item = new CheckboxMenuItem(name+" "+size); + item.setActionCommand("" + imp.getID()); + window.add(item); + item.addItemListener(ij); + } + + /** Removes the specified item from the Window menu. */ + static synchronized void removeWindowMenuItem(int index) { + //IJ.log("removeWindowMenuItem: "+index+" "+windowMenuItems2+" "+window.getItemCount()); + if (ij==null) + return; + try { + if (index>=0 && index-1) + label = label.substring(0, index); + } + if (item!=null && label.equals(oldLabel) && (imp==null||(""+imp.getID()).equals(cmd))) { + String size = ""; + if (imp!=null) + size = " " + ImageWindow.getImageSize(imp); + item.setLabel(newLabel+size); + return; + } + } + } catch (Exception e) {} + } + + /** Adds a file path to the beginning of the File/Open Recent submenu. */ + public static synchronized void addOpenRecentItem(String path) { + if (ij==null) return; + int count = openRecentMenu.getItemCount(); + for (int i=0; iImport"); break; + case SAVE_AS_MENU: menu = getMenu("File>Save As"); break; + case SHORTCUTS_MENU: menu = shortcutsMenu; break; + case ABOUT_MENU: menu = getMenu("Help>About Plugins"); break; + case FILTERS_MENU: menu = getMenu("Process>Filters"); break; + case TOOLS_MENU: menu = getMenu("Analyze>Tools"); break; + case UTILITIES_MENU: menu = utilitiesMenu; break; + default: return 0; + } + int code = convertShortcutToCode(shortcut); + MenuItem item; + boolean functionKey = code>=KeyEvent.VK_F1 && code<=KeyEvent.VK_F12; + if (code==0) + item = new MenuItem(command); + else if (functionKey) { + command += " [F"+(code-KeyEvent.VK_F1+1)+"]"; + shortcuts.put(new Integer(code),command); + item = new MenuItem(command); + } else { + shortcuts.put(new Integer(code),command); + int keyCode = code; + boolean shift = false; + if (keyCode>=265 && keyCode<=290) { + keyCode -= 200; + shift = true; + } + item = new MenuItem(command, new MenuShortcut(keyCode, shift)); + } + menu.add(item); + item.addActionListener(ij); + pluginsTable.put(command, plugin); + shortcut = code>0 && !functionKey?"["+shortcut+"]":""; + pluginsPrefs.addElement(menuCode+",\""+command+shortcut+"\","+plugin); + return NORMAL_RETURN; + } + + /** Deletes a command installed by Plugins/Shortcuts/Add Shortcut. */ + public static int uninstallPlugin(String command) { + boolean found = false; + for (Enumeration en=pluginsPrefs.elements(); en.hasMoreElements();) { + String cmd = (String)en.nextElement(); + if (cmd.contains(command)) { + boolean ok = pluginsPrefs.removeElement((Object)cmd); + found = true; + break; + } + } + if (found) + return NORMAL_RETURN; + else + return COMMAND_NOT_FOUND; + + } + + public static boolean commandInUse(String command) { + if (pluginsTable.get(command)!=null) + return true; + else + return false; + } + + public static int convertShortcutToCode(String shortcut) { + int code = 0; + int len = shortcut.length(); + if (len==2 && shortcut.charAt(0)=='F') { + code = KeyEvent.VK_F1+(int)shortcut.charAt(1)-49; + if (code>=KeyEvent.VK_F1 && code<=KeyEvent.VK_F9) + return code; + else + return 0; + } + if (len==3 && shortcut.charAt(0)=='F') { + code = KeyEvent.VK_F10+(int)shortcut.charAt(2)-48; + if (code>=KeyEvent.VK_F10 && code<=KeyEvent.VK_F12) + return code; + else + return 0; + } + if (len==2 && shortcut.charAt(0)=='N') { // numeric keypad + code = KeyEvent.VK_NUMPAD0+(int)shortcut.charAt(1)-48; + if (code>=KeyEvent.VK_NUMPAD0 && code<=KeyEvent.VK_NUMPAD9) + return code; + switch (shortcut.charAt(1)) { + case '/': return KeyEvent.VK_DIVIDE; + case '*': return KeyEvent.VK_MULTIPLY; + case '-': return KeyEvent.VK_SUBTRACT; + case '+': return KeyEvent.VK_ADD; + case '.': return KeyEvent.VK_DECIMAL; + default: return 0; + } + } + if (len!=1) + return 0; + int c = (int)shortcut.charAt(0); + if (c>=65&&c<=90) //A-Z + code = KeyEvent.VK_A+c-65 + 200; + else if (c>=97&&c<=122) //a-z + code = KeyEvent.VK_A+c-97; + else if (c>=48&&c<=57) //0-9 + code = KeyEvent.VK_0+c-48; + return code; + } + + void installStartupMacroSet() { + if (macrosPath==null) { + MacroInstaller.installFromJar("/macros/StartupMacros.txt"); + return; + } + String path = macrosPath + "StartupMacros.txt"; + File f = new File(path); + if (!f.exists()) { + path = macrosPath + "StartupMacros.ijm"; + f = new File(path); + if (!f.exists()) { + (new MacroInstaller()).installFromIJJar("/macros/StartupMacros.txt"); + return; + } + } else { + if ("StartupMacros.fiji.ijm".equals(f.getName())) + path = f.getPath(); + } + String libraryPath = macrosPath + "Library.txt"; + f = new File(libraryPath); + boolean isLibrary = f.exists(); + try { + MacroInstaller mi = new MacroInstaller(); + if (isLibrary) mi.installLibrary(libraryPath); + mi.installStartupMacros(path); + nMacros += mi.getMacroCount(); + } catch (Exception e) {} + } + + static boolean validShortcut(String shortcut) { + int len = shortcut.length(); + if (shortcut.equals("")) + return true; + else if (len==1) + return true; + else if (shortcut.startsWith("F") && (len==2 || len==3)) + return true; + else + return false; + } + + /** Returns 'true' if this keyboard shortcut is in use. */ + public static boolean shortcutInUse(String shortcut) { + int code = convertShortcutToCode(shortcut); + if (shortcuts.get(new Integer(code))!=null) + return true; + else + return false; + } + + /** Set the size (in points) used for the fonts in ImageJ menus. + Set the size to 0 to use the Java default size. */ + public static void setFontSize(int size) { + if (size<9 && size!=0) size = 9; + if (size>24) size = 24; + fontSize = size; + } + + /** Returns the size (in points) used for the fonts in ImageJ menus. Returns + 0 if the default font size is being used or if this is a Macintosh. */ + public static int getFontSize() { + return fontSize; + //return IJ.isMacintosh()?0:fontSize; + } + + public static Font getFont() { + if (menuFont==null) { + int size = fontSize==0?13:fontSize; + size = (int)Math.round(size*scale); + if (IJ.isWindows() && scale>1.0 && size>17) + size = 17; // Java resets size to 12 if you try to set it to 18 or greater + menuFont = new Font("SanSerif", Font.PLAIN, size); + } + //System.out.println("Menus.getFont: "+scale+" "+fontSize+" "+menuFont); + return menuFont; + } + + /** Called once when ImageJ quits. */ + public static void savePreferences(Properties prefs) { + if (pluginsPrefs==null) + return; + int index = 0; + for (Enumeration en=pluginsPrefs.elements(); en.hasMoreElements();) { + String key = "plugin" + (index/10)%10 + index%10; + String value = (String)en.nextElement(); + prefs.put(key, value); + index++; + } + int n = openRecentMenu.getItemCount(); + for (int i=0; i"); + if (index==-1 || index==menuPath.length()-1) + return; + String label = menuPath.substring(index+1, menuPath.length()); + menuPath = menuPath.substring(0, index); + pluginsTable.put(label, plugin); + addItem(getMenu(menuPath), label, 0, false); + } + +} diff --git a/src/ij/OtherInstance.java b/src/ij/OtherInstance.java new file mode 100644 index 0000000..17ba5ab --- /dev/null +++ b/src/ij/OtherInstance.java @@ -0,0 +1,241 @@ +package ij; + +import ij.IJ; +import ij.ImageJ; +import ij.Prefs; + +import ij.io.OpenDialog; +import ij.io.Opener; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import java.lang.reflect.Method; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; + +import java.util.Properties; + +/* + * This class tries to contact another instance on the same machine, started + * by the current user. If such an instance is found, the arguments are + * sent to that instance. If no such an instance is found, listen for clients. + * + * No need for extra security, as the stub (and its serialization) contain + * a hard-to-guess hash code. + * + * @author Johannes Schindelin + */ +public class OtherInstance { + private static final String DELIMETER = "~!~"; // Separates macro name and argument + + interface ImageJInstance extends Remote { + void sendArgument(String arg) throws RemoteException; + } + + static class Implementation implements ImageJInstance { + int counter = 0; + + public void sendArgument(String cmd) { + if (IJ.debugMode) IJ.log("SocketServer.sendArgument: \""+ cmd+"\""); + if (cmd.startsWith("open ")) + (new Opener()).openAndAddToRecent(cmd.substring(5)); + else if (cmd.startsWith("macro ")) { + String name = cmd.substring(6); + String name2 = name; + String arg = null; + int index = name2.indexOf(DELIMETER); + if (index!=-1) { + name = name2.substring(0, index); + arg = name2.substring(index+DELIMETER.length(), name2.length()); + } + IJ.runMacroFile(name, arg); + } else if (cmd.startsWith("run ")) + IJ.run(cmd.substring(4)); + else if (cmd.startsWith("eval ")) { + String rtn = IJ.runMacro(cmd.substring(5)); + if (rtn!=null) + System.out.print(rtn); + } else if (cmd.startsWith("user.dir ")) + OpenDialog.setDefaultDirectory(cmd.substring(9)); + } + } + + public static String getStubPath() { + String display = System.getenv("DISPLAY"); + if (display!=null) { + display = display.replace(':', '_'); + display = display.replace('/', '_'); + } + String tmpDir = System.getProperty("java.io.tmpdir"); + tmpDir = IJ.addSeparator(tmpDir); + return tmpDir + "ImageJ-" + + System.getProperty("user.name") + "-" + + (display == null ? "" : display + "-") + + ImageJ.getPort() + ".stub"; + } + + public static void makeFilePrivate(String path) { + try { + File file = new File(path); + file.deleteOnExit(); + + // File.setReadable() is Java 6 + Class[] types = { boolean.class, boolean.class }; + Method m = File.class.getMethod("setReadable", types); + Object[] arguments = { Boolean.FALSE, Boolean.FALSE }; + m.invoke(file, arguments); + arguments = new Object[] { Boolean.TRUE, Boolean.TRUE }; + m.invoke(file, arguments); + types = new Class[] { boolean.class }; + m = File.class.getMethod("setWritable", types); + arguments = new Object[] { Boolean.FALSE }; + m.invoke(file, arguments); + return; + } catch (Exception e) { + if (IJ.debugMode) + System.err.println("Java < 6 detected," + + " trying chmod 0600 " + path); + } + if (!IJ.isWindows()) { + try { + String[] command = { + "chmod", "0600", path + }; + Runtime.getRuntime().exec(command); + } catch (Exception e) { + if (IJ.debugMode) + System.err.println("Even chmod failed."); + } + } + } + + public static boolean sendArguments(String[] args) { + if (!isRMIEnabled()) + return false; + String file = getStubPath(); + try { + FileInputStream in = new FileInputStream(file); + ImageJInstance instance = (ImageJInstance) new ObjectInputStream(in).readObject(); + in.close(); + if (instance==null) + return false; + instance.sendArgument("user.dir "+System.getProperty("user.dir")); + int macros = 0; + for (int i=0; itext in the preferences + * file using the keyword key. The string can be + * retrieved using the appropriate get() method. + * @see #get(String,String) + */ + public static void set(String key, String text) { + if (key.indexOf('.')<1) + throw new IllegalArgumentException("Key must have a prefix"); + if (text==null) + ijPrefs.remove(KEY_PREFIX+key); + else + ijPrefs.put(KEY_PREFIX+key, text); + } + + /** Saves the value of the integer value in the preferences + * file using the keyword key. The value can be + * retrieved using the appropriate get() method. + * @see #get(String,double) + */ + public static void set(String key, int value) { + set(key, Integer.toString(value)); + } + + /** Saves the value of the double value in the preferences + * file using the keyword key. The value can be + * retrieved using the appropriate get() method. + * @see #get(String,double) + */ + public static void set(String key, double value) { + set(key, ""+value); + } + + /** Saves the value of the boolean value in the preferences + * file using the keyword key. The value can be + * retrieved using the appropriate get() method. + * @see #get(String,boolean) + */ + public static void set(String key, boolean value) { + set(key, ""+value); + } + + /** Uses the keyword key to retrieve a string from the + preferences file. Returns defaultValue if the key + is not found. */ + public static String get(String key, String defaultValue) { + String value = ijPrefs.getProperty(KEY_PREFIX+key); + if (value == null) + return defaultValue; + else + return value; + } + + /** Uses the keyword key to retrieve a number from the + preferences file. Returns defaultValue if the key + is not found. */ + public static double get(String key, double defaultValue) { + String s = ijPrefs.getProperty(KEY_PREFIX+key); + Double d = null; + if (s!=null) { + try {d = new Double(s);} + catch (NumberFormatException e) {d = null;} + if (d!=null) + return(d.doubleValue()); + } + return defaultValue; + } + + /** Uses the keyword key to retrieve a boolean from + the preferences file. Returns defaultValue if + the key is not found. */ + public static boolean get(String key, boolean defaultValue) { + String value = ijPrefs.getProperty(KEY_PREFIX+key); + if (value==null) + return defaultValue; + else + return value.equals("true"); + } + + /** Finds and loads the configuration file ("IJ_Props.txt") + * and the preferences file ("IJ_Prefs.txt"). + * @return an error message if "IJ_Props.txt" not found. + */ + public static String load(Object ij, Applet applet) { + if (ImageJDir==null) + ImageJDir = System.getProperty("user.dir"); + if (ij!=null) { + InputStream f = null; + try { // Look for IJ_Props.txt in ImageJ folder + f = new FileInputStream(ImageJDir+"/"+PROPS_NAME); + propertiesPath = ImageJDir+"/"+PROPS_NAME; + } catch (FileNotFoundException e) { + f = null; + } + if (f==null) { + // Look in ij.jar if not found in ImageJ folder + f = ij.getClass().getResourceAsStream("/"+PROPS_NAME); + } + if (applet!=null) + return loadAppletProps(f, applet); + if (f==null) + return PROPS_NAME+" not found in ij.jar or in "+ImageJDir; + f = new BufferedInputStream(f); + try { + props.load(f); + f.close(); + } catch (IOException e) { + return("Error loading "+PROPS_NAME); + } + imagesURL = props.getProperty(IJ.isJava18()?"images.location":"images.location2"); + } + loadPreferences(); + loadOptions(); + guiScale = get(GUI_SCALE, 1.0); + return null; + } + + /* + static void dumpPrefs() { + System.out.println(""); + Enumeration e = ijPrefs.keys(); + while (e.hasMoreElements()) { + String key = (String) e.nextElement(); + System.out.println(key+": "+ijPrefs.getProperty(key)); + } + } + */ + + static String loadAppletProps(InputStream f, Applet applet) { + if (f==null) + return PROPS_NAME+" not found in ij.jar"; + try { + props.load(f); + f.close(); + } + catch (IOException e) {return("Error loading "+PROPS_NAME);} + try { + URL url = new URL(applet.getDocumentBase(), "images/"); + imagesURL = url.toString(); + } + catch (Exception e) {} + return null; + } + + /** Returns the URL of the directory that contains the ImageJ sample images. */ + public static String getImagesURL() { + return imagesURL; + } + + /** Sets the URL of the directory that contains the ImageJ sample images. */ + public static void setImagesURL(String url) { + imagesURL = url; + } + + /** Obsolete, replaced by getImageJDir(), which, unlike this method, + returns a path that ends with File.separator. */ + public static String getHomeDir() { + return ImageJDir; + } + + /** Returns the path, ending in File.separator, to the ImageJ directory. */ + public static String getImageJDir() { + String path = Menus.getImageJPath(); + if (path==null) + return ImageJDir + File.separator; + else + return path; + } + + /** Returns the path to the directory where the + preferences file (IJPrefs.txt) is saved. */ + public static String getPrefsDir() { + if (prefsDir==null) { + if (ImageJDir==null) + ImageJDir = System.getProperty("user.dir"); + File f = new File(ImageJDir+File.separator+PREFS_NAME); + if (f.exists()) { + prefsDir = ImageJDir; + preferencesPath = ImageJDir+"/"+PREFS_NAME; + } + //System.out.println("getPrefsDir: "+f+" "+prefsDir); + if (prefsDir==null) { + String dir = System.getProperty("user.home"); + if (IJ.isMacOSX()) + dir += "/Library/Preferences"; + else + dir += File.separator+".imagej"; + prefsDir = dir; + } + } + return prefsDir; + } + + /** Sets the path to the ImageJ directory. */ + static void setHomeDir(String path) { + if (path.endsWith(File.separator)) + path = path.substring(0, path.length()-1); + ImageJDir = path; + } + + /** Returns the default directory, if any, or null. */ + public static String getDefaultDirectory() { + if (commandLineMacro) + return null; + else + return getString(DIR_IMAGE); + } + + /** Returns the file.separator system property. */ + public static String getFileSeparator() { + return separator; + } + + /** Opens the ImageJ preferences file ("IJ_Prefs.txt") file. */ + static void loadPreferences() { + String path = getPrefsDir()+separator+PREFS_NAME; + boolean ok = loadPrefs(path); + if (!ok) { // not found + if (IJ.isWindows()) + path = ImageJDir +separator+PREFS_NAME; + else + path = System.getProperty("user.home")+separator+PREFS_NAME; //User's home dir + ok = loadPrefs(path); + if (ok) + new File(path).delete(); + } + + } + + static boolean loadPrefs(String path) { + try { + InputStream is = new BufferedInputStream(new FileInputStream(path)); + ijPrefs.load(is); + is.close(); + return true; + } catch (Exception e) { + return false; + } + } + + /** Saves user preferences in the IJ_Prefs.txt properties file. */ + public static void savePreferences() { + String path = null; + commandLineMacro = false; + try { + Properties prefs = new Properties(); + String dir = OpenDialog.getDefaultDirectory(); + if (dir!=null) + prefs.put(DIR_IMAGE, dir); + prefs.put(ROICOLOR, Tools.c2hex(Roi.getColor())); + prefs.put(SHOW_ALL_COLOR, Tools.c2hex(ImageCanvas.getShowAllColor())); + prefs.put(FCOLOR, Tools.c2hex(Toolbar.getForegroundColor())); + prefs.put(BCOLOR, Tools.c2hex(Toolbar.getBackgroundColor())); + prefs.put(JPEG, Integer.toString(FileSaver.getJpegQuality())); + prefs.put(FPS, Double.toString(Animator.getFrameRate())); + prefs.put(DIV_BY_ZERO_VALUE, Double.toString(FloatBlitter.divideByZeroValue)); + prefs.put(NOISE_SD, Double.toString(Filters.getSD())); + if (threads>1) prefs.put(THREADS, Integer.toString(threads)); + if (IJ.isMacOSX()) useJFileChooser = false; + if (!IJ.isLinux()) dialogCancelButtonOnRight = false; + saveOptions(prefs); + savePluginPrefs(prefs); + ImageJ ij = IJ.getInstance(); + if (ij!=null) + ij.savePreferences(prefs); + Menus.savePreferences(prefs); + ParticleAnalyzer.savePreferences(prefs); + Analyzer.savePreferences(prefs); + ImportDialog.savePreferences(prefs); + PlotWindow.savePreferences(prefs); + NewImage.savePreferences(prefs); + String prefsDir = getPrefsDir(); + path = prefsDir+separator+PREFS_NAME; + if (prefsDir.endsWith(".imagej")) { + File f = new File(prefsDir); + if (!f.exists()) f.mkdir(); // create .imagej directory + } + if (resetPreferences) { + File f = new File(path); + if (!f.exists()) + IJ.error("Edit>Options>Reset", "Unable to reset preferences. File not found at\n"+path); + boolean rtn = f.delete(); + resetPreferences = false; + } else + savePrefs(prefs, path); + } catch (Throwable t) { + String msg = t.getMessage(); + if (msg==null) msg = ""+t; + int delay = 4000; + try { + new TextWindow("Error Saving Preferences:\n"+path, msg, 500, 200); + IJ.wait(delay); + } catch (Throwable t2) {} + } + } + + /** Delete the preferences file when ImageJ quits. */ + public static void resetPreferences() { + resetPreferences = true; + } + + static void loadOptions() { + int defaultOptions = ANTIALIASING+AVOID_RESLICE_INTERPOLATION+ANTIALIASED_TOOLS+MULTI_POINT_MODE + +(!IJ.isMacOSX()?RUN_SOCKET_LISTENER:0)+BLACK_BACKGROUND; + int options = getInt(OPTIONS, defaultOptions); + usePointerCursor = (options&USE_POINTER)!=0; + //antialiasedText = (options&ANTIALIASING)!=0; + antialiasedText = false; + interpolateScaledImages = (options&INTERPOLATE)!=0; + open100Percent = (options&ONE_HUNDRED_PERCENT)!=0; + blackBackground = (options&BLACK_BACKGROUND)!=0; + useJFileChooser = (options&JFILE_CHOOSER)!=0; + weightedColor = (options&WEIGHTED)!=0; + if (weightedColor) + ColorProcessor.setWeightingFactors(0.299, 0.587, 0.114); + blackCanvas = (options&BLACK_CANVAS)!=0; + requireControlKey = (options&REQUIRE_CONTROL)!=0; + useInvertingLut = (options&USE_INVERTING_LUT)!=0; + intelByteOrder = (options&INTEL_BYTE_ORDER)!=0; + noBorder = (options&NO_BORDER)!=0; + showAllSliceOnly = (options&SHOW_ALL_SLICE_ONLY)!=0; + copyColumnHeaders = (options©_HEADERS)!=0; + noRowNumbers = (options&NO_ROW_NUMBERS)!=0; + moveToMisc = (options&MOVE_TO_MISC)!=0; + runSocketListener = (options&RUN_SOCKET_LISTENER)!=0; + multiPointMode = (options&MULTI_POINT_MODE)!=0; + rotateYZ = (options&ROTATE_YZ)!=0; + flipXZ = (options&FLIP_XZ)!=0; + //dontSaveHeaders = (options&DONT_SAVE_HEADERS)!=0; + //dontSaveRowNumbers = (options&DONT_SAVE_ROW_NUMBERS)!=0; + noClickToGC = (options&NO_CLICK_TO_GC)!=0; + avoidResliceInterpolation = (options&AVOID_RESLICE_INTERPOLATION)!=0; + keepUndoBuffers = (options&KEEP_UNDO_BUFFERS)!=0; + + defaultOptions = (!IJ.isMacOSX()?USE_FILE_CHOOSER:0); + int options2 = getInt(OPTIONS2, defaultOptions); + useSystemProxies = (options2&USE_SYSTEM_PROXIES)!=0; + useFileChooser = (options2&USE_FILE_CHOOSER)!=0; + subPixelResolution = (options2&SUBPIXEL_RESOLUTION)!=0; + enhancedLineTool = (options2&ENHANCED_LINE_TOOL)!=0; + skipRawDialog = (options2&SKIP_RAW_DIALOG)!=0; + reverseNextPreviousOrder = (options2&REVERSE_NEXT_PREVIOUS_ORDER)!=0; + autoRunExamples = (options2&AUTO_RUN_EXAMPLES)!=0; + showAllPoints = (options2&SHOW_ALL_POINTS)!=0; + doNotSaveWindowLocations = (options2&DO_NOT_SAVE_WINDOW_LOCS)!=0; + jFileChooserSettingChanged = (options2&JFILE_CHOOSER_CHANGED)!=0; + dialogCancelButtonOnRight = (options2&CANCEL_BUTTON_ON_RIGHT)!=0; + ignoreRescaleSlope = (options2&IGNORE_RESCALE_SLOPE)!=0; + nonBlockingFilterDialogs = (options2&NON_BLOCKING_DIALOGS)!=0; + fixedDicomScaling = (options2&FIXED_DICOM_SCALINGg)!=0; + } + + static void saveOptions(Properties prefs) { + int options = (usePointerCursor?USE_POINTER:0) + (antialiasedText?ANTIALIASING:0) + + (interpolateScaledImages?INTERPOLATE:0) + (open100Percent?ONE_HUNDRED_PERCENT:0) + + (blackBackground?BLACK_BACKGROUND:0) + (useJFileChooser?JFILE_CHOOSER:0) + + (blackCanvas?BLACK_CANVAS:0) + (weightedColor?WEIGHTED:0) + + (requireControlKey?REQUIRE_CONTROL:0) + + (useInvertingLut?USE_INVERTING_LUT:0) + + (intelByteOrder?INTEL_BYTE_ORDER:0) + (doubleBuffer?DOUBLE_BUFFER:0) + + (noPointLabels?NO_POINT_LABELS:0) + (noBorder?NO_BORDER:0) + + (showAllSliceOnly?SHOW_ALL_SLICE_ONLY:0) + (copyColumnHeaders?COPY_HEADERS:0) + + (noRowNumbers?NO_ROW_NUMBERS:0) + (moveToMisc?MOVE_TO_MISC:0) + + (runSocketListener?RUN_SOCKET_LISTENER:0) + + (multiPointMode?MULTI_POINT_MODE:0) + (rotateYZ?ROTATE_YZ:0) + + (flipXZ?FLIP_XZ:0) + (dontSaveHeaders?DONT_SAVE_HEADERS:0) + + (dontSaveRowNumbers?DONT_SAVE_ROW_NUMBERS:0) + (noClickToGC?NO_CLICK_TO_GC:0) + + (avoidResliceInterpolation?AVOID_RESLICE_INTERPOLATION:0) + + (keepUndoBuffers?KEEP_UNDO_BUFFERS:0); + prefs.put(OPTIONS, Integer.toString(options)); + + int options2 = (useSystemProxies?USE_SYSTEM_PROXIES:0) + + (useFileChooser?USE_FILE_CHOOSER:0) + (subPixelResolution?SUBPIXEL_RESOLUTION:0) + + (enhancedLineTool?ENHANCED_LINE_TOOL:0) + (skipRawDialog?SKIP_RAW_DIALOG:0) + + (reverseNextPreviousOrder?REVERSE_NEXT_PREVIOUS_ORDER:0) + + (autoRunExamples?AUTO_RUN_EXAMPLES:0) + (showAllPoints?SHOW_ALL_POINTS:0) + + (doNotSaveWindowLocations?DO_NOT_SAVE_WINDOW_LOCS:0) + + (jFileChooserSettingChanged?JFILE_CHOOSER_CHANGED:0) + + (dialogCancelButtonOnRight?CANCEL_BUTTON_ON_RIGHT:0) + + (ignoreRescaleSlope?IGNORE_RESCALE_SLOPE:0) + + (nonBlockingFilterDialogs?NON_BLOCKING_DIALOGS:0) + + (fixedDicomScaling?FIXED_DICOM_SCALINGg:0); + prefs.put(OPTIONS2, Integer.toString(options2)); + } + + /** Saves the Point loc in the preferences + file as a string using the keyword key. */ + public static void saveLocation(String key, Point loc) { + if (!doNotSaveWindowLocations) + set(key, loc!=null?loc.x+","+loc.y:null); + } + + /** Uses the keyword key to retrieve a location + from the preferences file. Returns null if the + key is not found or the location is not valid (e.g., offscreen). */ + public static Point getLocation(String key) { + String value = ijPrefs.getProperty(KEY_PREFIX+key); + if (value==null) return null; + int index = value.indexOf(","); + if (index==-1) return null; + double xloc = Tools.parseDouble(value.substring(0, index)); + if (Double.isNaN(xloc) || index==value.length()-1) return null; + double yloc = Tools.parseDouble(value.substring(index+1)); + if (Double.isNaN(yloc)) return null; + Point p = new Point((int)xloc, (int)yloc); + Rectangle bounds = GUI.getScreenBounds(p); // get bounds of screen that contains p + if (bounds!=null && p.x+100<=bounds.x+bounds.width && p.y+ 40<=bounds.y+bounds.height) { + if (locKeys.get(key)==null) { // first time for this key? + locKeys.setProperty(key, ""); + Rectangle primaryScreen = GUI.getMaxWindowBounds(); + ImageJ ij = IJ.getInstance(); + Point ijLoc = ij!=null?ij.getLocation():null; + //System.out.println("getLoc: "+key+" "+(ijLoc!=null&&primaryScreen.contains(ijLoc)) + " "+!primaryScreen.contains(p)); + if ((ijLoc!=null&&primaryScreen.contains(ijLoc)) && !primaryScreen.contains(p)) + return null; // return null if "ImageJ" window on primary screen and this location is not + } + return p; + } else + return null; + } + + /** Save plugin preferences. */ + static void savePluginPrefs(Properties prefs) { + Enumeration e = ijPrefs.keys(); + while (e.hasMoreElements()) { + String key = (String) e.nextElement(); + if (key.indexOf(KEY_PREFIX) == 0) + prefs.put(key, ijPrefs.getProperty(key)); + } + } + + public static void savePrefs(Properties prefs, String path) throws IOException{ + FileOutputStream fos = new FileOutputStream(path); + BufferedOutputStream bos = new BufferedOutputStream(fos); + prefs.store(bos, "ImageJ "+ImageJ.VERSION+" Preferences"); + bos.close(); + } + + /** Returns the number of threads used by PlugInFilters to process images and stacks. */ + public static int getThreads() { + if (threads==0) { + threads = getInt(THREADS, 0); + int processors = Runtime.getRuntime().availableProcessors(); + if (threads<1 || threads>processors) + threads = processors; + } + return threads; + } + + /** Sets the number of threads (1-32) used by PlugInFilters to process stacks. */ + public static void setThreads(int n) { + if (n<1) n = 1; + threads = n; + } + + /** Sets the transparent index (0-255), or set to -1 to disable transparency. */ + public static void setTransparentIndex(int index) { + if (index<-1 || index>255) index = -1; + transparentIndex = index; + } + + /** Returns the transparent index (0-255), or -1 if transparency is disabled. */ + public static int getTransparentIndex() { + return transparentIndex; + } + + public static Properties getControlPanelProperties() { + return ijPrefs; + } + + public static String defaultResultsExtension() { + return get("options.ext", ".csv"); + } + + /** Sets the GenericDialog and Command Finder text scale (0.5 to 3.0). */ + public static void setGuiScale(double scale) { + if (scale>=0.5 && scale<=3.0) { + guiScale = scale; + set(GUI_SCALE, guiScale); + Roi.resetDefaultHandleSize(); + } + } + + /** Returns the GenericDialog and Command Finder text scale. */ + public static double getGuiScale() { + return guiScale; + } + + /** Returns the custom properties (IJ_Props.txt) file path. */ + public static String getCustomPropsPath() { + return propertiesPath; + } + + /** Returns the custom preferences (IJ_Prefs.txt) file path. */ + public static String getCustomPrefsPath() { + return preferencesPath; + } + + /** Retrieves a string from IJ_Props or IJ_Prefs.txt. + Does not retrieve strings set using Prefs.set(). */ + public static String getString(String key, String defaultString) { + if (props==null) + return defaultString; + String s = props.getProperty(key); + if (s==null) + return defaultString; + else + return s; + } + + /** Retrieves a string from string in IJ_Props or IJ_Prefs.txt. */ + public static String getString(String key) { + return props.getProperty(key); + } + + /** Retrieves a number from IJ_Props or IJ_Prefs.txt. + Does not retrieve numbers set using Prefs.set(). */ + public static int getInt(String key, int defaultValue) { + if (props==null) //workaround for Netscape JIT bug + return defaultValue; + String s = props.getProperty(key); + if (s!=null) { + try { + return Integer.decode(s).intValue(); + } catch (NumberFormatException e) {IJ.log(""+e);} + } + return defaultValue; + } + + /** Retrieves a number from IJ_Props or IJ_Prefs.txt. + Does not retrieve numbers set using Prefs.set(). */ + public static double getDouble(String key, double defaultValue) { + if (props==null) + return defaultValue; + String s = props.getProperty(key); + Double d = null; + if (s!=null) { + try {d = new Double(s);} + catch (NumberFormatException e){d = null;} + if (d!=null) + return(d.doubleValue()); + } + return defaultValue; + } + + /** Retrieves a boolean from IJ_Props or IJ_Prefs.txt. + Does not retrieve boolean set using Prefs.set(). */ + public static boolean getBoolean(String key, boolean defaultValue) { + if (props==null) return defaultValue; + String s = props.getProperty(key); + if (s==null) + return defaultValue; + else + return s.equals("true"); + } + + /** Finds a color in IJ_Props or IJ_Prefs.txt. */ + public static Color getColor(String key, Color defaultColor) { + int i = getInt(key, 0xaaa); + if (i == 0xaaa) + return defaultColor; + return new Color((i >> 16) & 0xFF, (i >> 8) & 0xFF, i & 0xFF); + } + + public static boolean commandLineMacro() { + return commandLineMacro; + } + +} + diff --git a/src/ij/RecentOpener.java b/src/ij/RecentOpener.java new file mode 100644 index 0000000..8f04162 --- /dev/null +++ b/src/ij/RecentOpener.java @@ -0,0 +1,37 @@ +package ij; +import ij.io.*; +import java.awt.*; +import java.io.*; + +/** Opens, in a separate thread, files selected from the File/Open Recent submenu.*/ +public class RecentOpener implements Runnable { + private String path; + + RecentOpener(String path) { + this.path = path; + Thread thread = new Thread(this, "RecentOpener"); + thread.start(); + } + + /** Open the file and move the path to top of the submenu. */ + public void run() { + Opener o = new Opener(); + o.open(path); + Menu menu = Menus.getOpenRecentMenu(); + int n = menu.getItemCount(); + int index = 0; + for (int i=0; i0) { + MenuItem item = menu.getItem(index); + menu.remove(index); + menu.insert(item, 0); + } + } + +} + diff --git a/src/ij/Undo.java b/src/ij/Undo.java new file mode 100644 index 0000000..5bca876 --- /dev/null +++ b/src/ij/Undo.java @@ -0,0 +1,215 @@ +/**Implements the Edit/Undo command.*/ + +package ij; +import ij.process.*; +import ij.gui.*; +import ij.measure.Calibration; +import java.awt.*; +import java.awt.image.*; + +/** This class consists of static methods and + fields that implement ImageJ's Undo command. */ +public class Undo { + + public static final int NOTHING = 0; + /** Undo using ImageProcessor.snapshot. */ + public static final int FILTER = 1; + /** Undo using an ImageProcessor copy. */ + public static final int TYPE_CONVERSION = 2; + public static final int PASTE = 3; + public static final int COMPOUND_FILTER = 4; + public static final int COMPOUND_FILTER_DONE = 5; + /** Undo using a single image, or composite color stack, copy (limited to 200MB). */ + public static final int TRANSFORM = 6; + public static final int OVERLAY_ADDITION = 7; + public static final int ROI = 8; + public static final int MACRO = 9; + + private static int whatToUndo = NOTHING; + private static int imageID; + private static ImageProcessor ipCopy = null; + private static ImagePlus impCopy; + private static Calibration calCopy; + private static Roi roiCopy; + private static double displayRangeMin, displayRangeMax; + private static LUT lutCopy; + private static Overlay overlayCopy; + + public static void setup(int what, ImagePlus imp) { + if (imp==null) { + whatToUndo = NOTHING; + reset(); + return; + } + if (IJ.debugMode) IJ.log("Undo.setup: "+what+" "+imp); + if (what==FILTER && whatToUndo==COMPOUND_FILTER) + return; + if (what==COMPOUND_FILTER_DONE) { + if (whatToUndo==COMPOUND_FILTER) + whatToUndo = what; + return; + } + whatToUndo = what; + imageID = imp.getID(); + if (what==TYPE_CONVERSION) { + ipCopy = imp.getProcessor(); + calCopy = (Calibration)imp.getCalibration().clone(); + } else if (what==TRANSFORM) { + if ((!IJ.macroRunning()||Prefs.supportMacroUndo) && (imp.getStackSize()==1||imp.getDisplayMode()==IJ.COMPOSITE) && imp.getSizeInBytes()<209715200) + impCopy = imp.duplicate(); + else + reset(); + } else if (what==MACRO) { + ipCopy = imp.getProcessor().duplicate(); + calCopy = (Calibration)imp.getCalibration().clone(); + impCopy = null; + } else if (what==COMPOUND_FILTER) { + ImageProcessor ip = imp.getProcessor(); + if (ip!=null) + ipCopy = ip.duplicate(); + else + ipCopy = null; + } else if (what==OVERLAY_ADDITION) { + impCopy = null; + ipCopy = null; + } else if (what==ROI) { + impCopy = null; + ipCopy = null; + Roi roi = imp.getRoi(); + if (roi!=null) { + roiCopy = (Roi)roi.clone(); + roiCopy.setImage(null); + } else + whatToUndo = NOTHING; + } else { + ipCopy = null; + ImageProcessor ip = imp.getProcessor(); + //lutCopy = (LUT)ip.getLut().clone(); + } + } + + public static void saveOverlay(ImagePlus imp) { + Overlay overlay = imp!=null?imp.getOverlay():null; + if (overlay!=null) + overlayCopy = overlay.duplicate(); + else + overlayCopy = null; + } + + public static void reset() { + if (IJ.debugMode) IJ.log("Undo.reset: "+ whatToUndo+" "+impCopy); + if (whatToUndo==COMPOUND_FILTER || whatToUndo==OVERLAY_ADDITION) + return; + whatToUndo = NOTHING; + imageID = 0; + ipCopy = null; + impCopy = null; + calCopy = null; + roiCopy = null; + lutCopy = null; + overlayCopy = null; + } + + public static void undo() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (IJ.debugMode) IJ.log("Undo.undo: "+ whatToUndo+" "+imp+" "+impCopy); + if (imp==null || imageID!=imp.getID()) { + if (imp!=null && !IJ.macroRunning()) { // does image still have an undo buffer? + ImageProcessor ip2 = imp.getProcessor(); + ip2.swapPixelArrays(); + imp.updateAndDraw(); + } else + reset(); + return; + } + switch (whatToUndo) { + case FILTER: + if (overlayCopy!=null) { + Overlay overlay = imp.getOverlay(); + if (overlay!=null) { + imp.setOverlay(overlayCopy); + overlayCopy = overlay.duplicate(); + } + } + ImageProcessor ip = imp.getProcessor(); + if (ip!=null) { + if (!IJ.macroRunning()) { + ip.swapPixelArrays(); + imp.updateAndDraw(); + return; // don't reset + } else { + ip.reset(); + imp.updateAndDraw(); + } + } + break; + case TYPE_CONVERSION: + case COMPOUND_FILTER: + case COMPOUND_FILTER_DONE: + if (ipCopy!=null) { + if (whatToUndo==TYPE_CONVERSION && calCopy!=null) + imp.setCalibration(calCopy); + if (swapImages(new ImagePlus("",ipCopy), imp)) { + imp.updateAndDraw(); + return; + } else + imp.setProcessor(null, ipCopy); + } + break; + case TRANSFORM: + if (impCopy!=null) + imp.setStack(impCopy.getStack()); + break; + case PASTE: + Roi roi = imp.getRoi(); + if (roi!=null) + roi.abortPaste(); + break; + case ROI: + Roi roiCopy2 = roiCopy; + setup(ROI, imp); // setup redo + imp.setRoi(roiCopy2); + return; //don't reset + case MACRO: + if (ipCopy!=null) { + imp.setProcessor(ipCopy); + if (calCopy!=null) imp.setCalibration(calCopy); + } + break; + case OVERLAY_ADDITION: + Overlay overlay = imp.getOverlay(); + if (overlay==null) + {IJ.beep(); return;} + int size = overlay.size(); + if (size>0) { + overlay.remove(size-1); + imp.draw(); + } else { + IJ.beep(); + return; + } + return; //don't reset + } + reset(); + } + + static boolean swapImages(ImagePlus imp1, ImagePlus imp2) { + if (imp1.getWidth()!=imp2.getWidth() || imp1.getHeight()!=imp2.getHeight() + || imp1.getBitDepth()!=imp2.getBitDepth() || IJ.macroRunning()) + return false; + ImageProcessor ip1 = imp1.getProcessor(); + ImageProcessor ip2 = imp2.getProcessor(); + double min1 = ip1.getMin(); + double max1 = ip1.getMax(); + double min2 = ip2.getMin(); + double max2 = ip2.getMax(); + ip2.setSnapshotPixels(ip1.getPixels()); + ip2.swapPixelArrays(); + ip1.setPixels(ip2.getSnapshotPixels()); + ip2.setSnapshotPixels(null); + ip1.setMinAndMax(min2, max2); + ip2.setMinAndMax(min1, max1); + return true; + } + +} diff --git a/src/ij/VirtualStack.java b/src/ij/VirtualStack.java new file mode 100644 index 0000000..658badf --- /dev/null +++ b/src/ij/VirtualStack.java @@ -0,0 +1,335 @@ +package ij; +import ij.process.*; +import ij.io.*; +import ij.gui.ImageCanvas; +import ij.util.Tools; +import ij.plugin.FolderOpener; +import java.io.*; +import java.awt.*; +import java.awt.image.ColorModel; +import java.util.Properties; + +/** This class represents an array of disk-resident images. */ +public class VirtualStack extends ImageStack { + private static final int INITIAL_SIZE = 100; + private String path; + private int nSlices; + private String[] names; + private String[] labels; + private int bitDepth; + private int delay; + private Properties properties; + private boolean generateData; + private int[] indexes; // used to translate non-CZT hyperstack slice numbers + + + /** Default constructor. */ + public VirtualStack() { } + + public VirtualStack(int width, int height) { + super(width, height); + } + + /** Creates an empty virtual stack. + * @param width image width + * @param height image height + * @param cm ColorModel or null + * @param path file path of directory containing the images + * @see #addSlice(String) + * @see OpenAsVirtualStack.js + */ + public VirtualStack(int width, int height, ColorModel cm, String path) { + super(width, height, cm); + path = IJ.addSeparator(path); + this.path = path; + names = new String[INITIAL_SIZE]; + labels = new String[INITIAL_SIZE]; + } + + /** Creates a virtual stack with no backing storage.
+ * See: Help>Examples>JavaScript>Terabyte VirtualStack + */ + public VirtualStack(int width, int height, int slices) { + this(width, height, slices, "8-bit"); + } + + /** Creates a virtual stack with no backing storage.
+ * See: Help>Examples>JavaScript>Terabyte VirtualStack + */ + public VirtualStack(int width, int height, int slices, String options) { + super(width, height, null); + nSlices = slices; + int depth = 8; + if (options.contains("16-bit")) depth=16; + if (options.contains("RGB")) depth=24; + if (options.contains("32-bit")) depth=32; + if (options.contains("delay")) delay=250; + this.generateData = options.contains("fill"); + this.bitDepth = depth; + } + + /** Adds an image to the end of a virtual stack created using the + * VirtualStack(w,h,cm,path) constructor. The argument + * can be a full file path (e.g., "C:/Users/wayne/dir1/image.tif") + * if the 'path' argument in the constructor is "". File names + * that start with '.' are ignored. + */ + public void addSlice(String fileName) { + if (fileName==null) + throw new IllegalArgumentException("'fileName' is null!"); + if (fileName.startsWith(".")) + return; + if (names==null) + throw new IllegalArgumentException("VirtualStack(w,h,cm,path) constructor not used"); + nSlices++; + if (nSlices==names.length) { + String[] tmp = new String[nSlices*2]; + System.arraycopy(names, 0, tmp, 0, nSlices); + names = tmp; + tmp = new String[nSlices*2]; + System.arraycopy(labels, 0, tmp, 0, nSlices); + labels = tmp; + } + names[nSlices-1] = fileName; + } + + /** Does nothing. */ + public void addSlice(String sliceLabel, Object pixels) { + } + + /** Does nothing.. */ + public void addSlice(String sliceLabel, ImageProcessor ip) { + } + + /** Does noting. */ + public void addSlice(String sliceLabel, ImageProcessor ip, int n) { + } + + /** Deletes the specified slice, were 1<=n<=nslices. */ + public void deleteSlice(int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException("Argument out of range: "+n); + if (nSlices<1) + return; + for (int i=n; i0) + deleteSlice(n); + } + + /** Returns the pixel array for the specified slice, were 1<=n<=nslices. */ + public Object getPixels(int n) { + ImageProcessor ip = getProcessor(n); + if (ip!=null) + return ip.getPixels(); + else + return null; + } + + /** Assigns a pixel array to the specified slice, + were 1<=n<=nslices. */ + public void setPixels(Object pixels, int n) { + } + + /** Returns an ImageProcessor for the specified slice, + were 1<=n<=nslices. Returns null if the stack is empty. + */ + public ImageProcessor getProcessor(int n) { + if (path==null) { //Help>Examples?JavaScript>Terabyte VirtualStack + ImageProcessor ip = null; + int w=getWidth(), h=getHeight(); + switch (bitDepth) { + case 8: ip = new ByteProcessor(w,h); break; + case 16: ip = new ShortProcessor(w,h); break; + case 24: ip = new ColorProcessor(w,h); break; + case 32: ip = new FloatProcessor(w,h); break; + } + String hlabel = null; + if (generateData) { + int value = 0; + ImagePlus img = WindowManager.getCurrentImage(); + if (img!=null && img.getStackSize()==nSlices) + value = img.getCurrentSlice()-1; + if (bitDepth==16) + value *= 256; + if (bitDepth!=32) { + for (int i=0; i0) + IJ.wait(delay); + return ip; + } + n = translate(n); // update n for hyperstacks not in the default CZT order + Opener opener = new Opener(); + opener.setSilentMode(true); + IJ.redirectErrorMessages(true); + ImagePlus imp = opener.openTempImage(path, names[n-1]); + IJ.redirectErrorMessages(false); + ImageProcessor ip = null; + int depthThisImage = 0; + if (imp!=null) { + int w = imp.getWidth(); + int h = imp.getHeight(); + int type = imp.getType(); + ColorModel cm = imp.getProcessor().getColorModel(); + String info = (String)imp.getProperty("Info"); + if (info!=null) { + if (FolderOpener.useInfo(info)) + labels[n-1] = info; + } else { + String sliceLabel = imp.getStack().getSliceLabel(1); + if (FolderOpener.useInfo(sliceLabel)) + labels[n-1] = "Label: "+sliceLabel; + } + depthThisImage = imp.getBitDepth(); + ip = imp.getProcessor(); + ip.setOverlay(imp.getOverlay()); + properties = imp.getProperty("FHT")!=null?imp.getProperties():null; + } else { + File f = new File(path, names[n-1]); + String msg = f.exists()?"Error opening ":"File not found: "; + ip = new ByteProcessor(getWidth(), getHeight()); + ip.invert(); + label(ip, msg+names[n-1], Color.black); + depthThisImage = 8; + } + if (depthThisImage!=bitDepth) { + switch (bitDepth) { + case 8: ip=ip.convertToByte(true); break; + case 16: ip=ip.convertToShort(true); break; + case 24: ip=ip.convertToRGB(); break; + case 32: ip=ip.convertToFloat(); break; + } + } + if (ip.getWidth()!=getWidth() || ip.getHeight()!=getHeight()) { + ImageProcessor ip2 = ip.createProcessor(getWidth(), getHeight()); + ip2.insert(ip, 0, 0); + ip = ip2; + } + return ip; + } + + private void label(ImageProcessor ip, String msg, Color color) { + int size = getHeight()/20; + if (size<9) size=9; + Font font = new Font("Helvetica", Font.PLAIN, size); + ip.setFont(font); + ip.setAntialiasedText(true); + ip.setColor(color); + ip.drawString(msg, size, size*2); + } + + /** Currently not implemented */ + public int saveChanges(int n) { + return -1; + } + + /** Returns the number of slices in this stack. */ + public int size() { + return getSize(); + } + + public int getSize() { + return nSlices; + } + + /** Returns the label of the Nth image. */ + public String getSliceLabel(int n) { + if (labels==null) + return null; + String label = labels[n-1]; + if (label==null) + return names[n-1]; + else { + if (label.startsWith("Label: ")) // slice label + return label.substring(7,label.length()); + else + return names[n-1]+"\n"+label; + } + } + + /** Returns null. */ + public Object[] getImageArray() { + return null; + } + + /** Does nothing. */ + public void setSliceLabel(String label, int n) { + } + + /** Always return true. */ + public boolean isVirtual() { + return true; + } + + /** Does nothing. */ + public void trim() { + } + + /** Returns the path to the directory containing the images. */ + public String getDirectory() { + return IJ.addSeparator(path); + } + + /** Returns the file name of the specified slice, were 1<=n<=nslices. */ + public String getFileName(int n) { + return names[n-1]; + } + + /** Sets the bit depth (8, 16, 24 or 32). */ + public void setBitDepth(int bitDepth) { + this.bitDepth = bitDepth; + } + + /** Returns the bit depth (8, 16, 24 or 32), or 0 if the bit depth is not known. */ + public int getBitDepth() { + return bitDepth; + } + + public ImageStack sortDicom(String[] strings, String[] info, int maxDigits) { + int n = size(); + String[] names2 = new String[n]; + for (int i=0; i0) { + ImagePlus imp = getFocusManagerActiveImage(); + if (imp!=null) + return imp; + ImageWindow win = (ImageWindow)imageList.get(imageList.size()-1); + return win.getImagePlus(); + } else + return Interpreter.getLastBatchModeImage(); + } + + private static ImagePlus getFocusManagerActiveImage() { + if (IJ.isMacro()) + return null; + KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); + Window win = kfm.getActiveWindow(); + ImagePlus imp = null; + if (win!=null && (win instanceof ImageWindow)) + imp = ((ImageWindow)win).getImagePlus(); + return imp; + } + + /** Returns the number of open image windows. */ + public static int getWindowCount() { + int count = imageList.size(); + return count; + } + + /** Returns the number of open images. */ + public static int getImageCount() { + int count = imageList.size(); + count += Interpreter.getBatchModeImageCount(); + if (count==0 && getCurrentImage()!=null) + count = 1; + return count; + } + + /** Returns the front most window or null. */ + public static Window getActiveWindow() { + return frontWindow; + } + + /** Returns the Window containing the active table, or null. + * @see ij.measure.ResultsTable#getActiveTable + */ + public static Window getActiveTable() { + return frontTable; + } + + /** Obsolete; replaced by getActiveWindow. */ + public static Frame getFrontWindow() { + return frontFrame; + } + + /** Returns a list of the IDs of open images. Returns + null if no image windows are open. */ + public synchronized static int[] getIDList() { + int nWindows = imageList.size(); + int[] batchModeImages = Interpreter.getBatchModeImageIDs(); + int nBatchImages = batchModeImages.length; + if ((nWindows+nBatchImages)==0) + return null; + int[] list = new int[nWindows+nBatchImages]; + for (int i=0; i0) + imageID = getNthImageID(imageID); + if (imageID==0 || getImageCount()==0) + return null; + ImagePlus imp2 = Interpreter.getBatchModeImage(imageID); + if (imp2!=null) + return imp2; + ImagePlus imp = null; + for (int i=0; ilist.length) + return 0; + else + return list[n-1]; + } else { + if (n>imageList.size()) return 0; + ImageWindow win = (ImageWindow)imageList.get(n-1); + if (win!=null) + return win.getImagePlus().getID(); + else + return 0; + } + } + + + /** Returns the first image that has the specified title or null if it is not found. */ + public synchronized static ImagePlus getImage(String title) { + int[] wList = getIDList(); + if (wList==null) return null; + for (int i=0; i=0) { + Menus.removeWindowMenuItem(index); + nonImageList.removeElement(win); + } + if (win!=null && win==frontTable) + frontTable = null; + } + setWindow(null); + } + + /** Removes the specified Frame from the Window menu. */ + public static void removeWindow(Frame win) { + removeWindow((Window)win); + } + + private static void removeImageWindow(ImageWindow win) { + int index = imageList.indexOf(win); + if (index==-1) + return; // not on the window list + try { + synchronized(WindowManager.class) { + imageList.remove(win); + } + activations.remove(win); + if (imageList.size()>1 && !Prefs.closingAll) { + ImageWindow win2 = activations.size()>0?(ImageWindow)activations.get(activations.size()-1):null; + setCurrentWindow(win2); + } else + currentWindow = null; + setTempCurrentImage(null); //??? + int nonImageCount = nonImageList.size(); + if (nonImageCount>0) + nonImageCount++; + Menus.removeWindowMenuItem(nonImageCount+index); + Menus.updateMenus(); + Undo.reset(); + } catch (Exception e) { } + } + + /** The specified Window becomes the front window. */ + public static void setWindow(Window win) { + //System.out.println("setWindow(W): "+win); + frontWindow = win; + if (win instanceof Frame) + frontFrame = (Frame)win; + } + + /** The specified frame becomes the front window, the one returnd by getFrontWindow(). */ + public static void setWindow(Frame win) { + frontWindow = win; + frontFrame = win; + if (win!=null && win instanceof TextWindow && !(win instanceof Editor) && !"Log".equals(((TextWindow)win).getTitle())) + frontTable = win; + //System.out.println("Set window(F): "+(win!=null?win.getTitle():"null")); + } + + /** Closes all windows. Stops and returns false if an image or Editor "save changes" dialog is canceled. */ + public synchronized static boolean closeAllWindows() { + Prefs.closingAll = true; + while (imageList.size()>0) { + if (!((ImageWindow)imageList.get(0)).close()) { + Prefs.closingAll = false; + return false; + } + if (!quittingViaMacro()) + IJ.wait(100); + } + Prefs.closingAll = false; + Frame[] nonImages = getNonImageWindows(); + for (int i=0; i0) // remove image size (e.g., " 90K") + menuItemLabel = menuItemLabel.substring(0, lastSpace); + String idString = item.getActionCommand(); + int id = (int)Tools.parseDouble(idString, 0); + ImagePlus imp = WindowManager.getImage(id); + if (imp==null) return; + ImageWindow win1 = imp.getWindow(); + if (win1==null) return; + setCurrentWindow(win1); + toFront(win1); + int index = imageList.indexOf(win1); + int n = Menus.window.getItemCount(); + int start = Menus.WINDOW_MENU_ITEMS+Menus.windowMenuItems2; + for (int j=start; jHEADLESS) + defaultStyle = FILLED; + } + + public Arrow(double ox1, double oy1, double ox2, double oy2) { + super(ox1, oy1, ox2, oy2); + setStrokeWidth(2); + } + + public Arrow(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + setStrokeWidth(defaultWidth); + style = defaultStyle; + headSize = defaultHeadSize; + doubleHeaded = defaultDoubleHeaded; + outline = defaultOutline; + setStrokeColor(Toolbar.getForegroundColor()); + } + + /** Draws this arrow on the image. */ + public void draw(Graphics g) { + Shape shape2 = null; + if (doubleHeaded) { + flipEnds(); + shape2 = getShape(); + flipEnds(); + } + Shape shape = getShape(); + Color color = strokeColor!=null? strokeColor:ROIColor; + if (fillColor!=null) color = fillColor; + g.setColor(color); + Graphics2D g2 = (Graphics2D)g; + setRenderingHint(g2); + AffineTransform at = g2.getDeviceConfiguration().getDefaultTransform(); + double mag = getMagnification(); + int xbase=0, ybase=0; + if (ic!=null) { + Rectangle r = ic.getSrcRect(); + xbase = r.x; ybase = r.y; + } + at.setTransform(mag, 0.0, 0.0, mag, (-xbase+0.5)*mag, (-ybase+0.5)*mag); //0.5: int coordinate at pixel center + if (outline) { + float lineWidth = (float)(getOutlineWidth()*mag); + g2.setStroke(new BasicStroke(lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND)); + g2.draw(at.createTransformedShape(shape)); + if (doubleHeaded) g2.draw(at.createTransformedShape(shape2)); + g2.setStroke(defaultStroke); + } else { + g2.fill(at.createTransformedShape(shape)); + if (doubleHeaded) g2.fill(at.createTransformedShape(shape2)); + } + if (!overlay) { + handleColor=Color.white; + drawHandle(g, screenXD(x1d), screenYD(y1d)); + drawHandle(g, screenXD(x2d), screenYD(y2d)); + drawHandle(g, screenXD(x1d+(x2d-x1d)/2.0), screenYD(y1d+(y2d-y1d)/2.0)); + } + if (state!=NORMAL && imp!=null && imp.getRoi()!=null) + showStatus(); + if (updateFullWindow) + {updateFullWindow = false; imp.draw();} + } + + private void flipEnds() { + double tmp = x1R; + x1R=x2R; + x2R=tmp; + tmp=y1R; + y1R=y2R; + y2R=tmp; + } + + private Shape getPath() { + path.reset(); + path = new GeneralPath(); + calculatePoints(); + float tailx = points[0]; + float taily = points[1]; + float headbackx = points[2*1]; + float headbacky = points[2*1+1]; + float headtipx = points[2*3]; + float headtipy = points[2*3+1]; + if (outline) { + double dx = headtipx - tailx; + double dy = headtipy - taily; + double shaftLength = Math.sqrt(dx*dx+dy*dy); + dx = headtipx - headbackx; + dy = headtipy- headbacky; + double headLength = Math.sqrt(dx*dx+dy*dy); + headShaftRatio = headLength/shaftLength; + if (headShaftRatio>1.0) + headShaftRatio = 1.0; + //IJ.log(headShaftRatio+" "+(int)shaftLength+" "+(int)headLength+" "+(int)tailx+" "+(int)taily+" "+(int)headtipx+" "+(int)headtipy); + } + path.moveTo(tailx, taily); // tail + path.lineTo(headbackx, headbacky); // head back + path.moveTo(headbackx, headbacky); // head back + if (style==OPEN) + path.moveTo(points[2 * 2], points[2 * 2 + 1]); + else + path.lineTo(points[2 * 2], points[2 * 2 + 1]); // left point + path.lineTo(headtipx, headtipy); // head tip + path.lineTo(points[2 * 4], points[2 * 4 + 1]); // right point + path.lineTo(headbackx, headbacky); // back to the head back + return path; + } + + /** Based on the method with the same name in Fiji's Arrow plugin, + written by Jean-Yves Tinevez and Johannes Schindelin. */ + private void calculatePoints() { + double tip = 0.0; + double base; + double shaftWidth = getStrokeWidth(); + double length = 8+10*shaftWidth*0.5; + length = length*(headSize/10.0); + length -= shaftWidth*1.42; + if (style==NOTCHED) length*=0.74; + if (style==OPEN) length*=1.32; + if (length<0.0 || style==HEADLESS) length=0.0; + double x = getXBase(); + double y = getYBase(); + x1d=x+x1R; y1d=y+y1R; x2d=x+x2R; y2d=y+y2R; + x1=(int)x1d; y1=(int)y1d; x2=(int)x2d; y2=(int)y2d; + double dx=x2d-x1d, dy=y2d-y1d; + double arrowLength = Math.sqrt(dx*dx+dy*dy); + dx=dx/arrowLength; dy=dy/arrowLength; + if (doubleHeaded && style!=HEADLESS) { + points[0] = (float)(x1d+dx*shaftWidth*2.0); + points[1] = (float)(y1d+dy*shaftWidth*2.0); + } else { + points[0] = (float)x1d; + points[1] = (float)y1d; + } + if (length>0) { + double factor = style==OPEN?1.3:1.42; + points[2*3] = (float)(x2d-dx*shaftWidth*factor); + points[2*3+1] = (float)(y2d-dy*shaftWidth*factor); + if (style==BAR) { + points[2*3] = (float)(x2d-dx*shaftWidth*0.5); + points[2*3+1] = (float)(y2d-dy*shaftWidth*0.5); + } + } else { + points[2*3] = (float)x2d; + points[2*3+1] = (float)y2d; + } + final double alpha = Math.atan2(points[2*3+1]-points[1], points[2*3]-points[0]); + double SL = 0.0; + switch (style) { + case FILLED: case HEADLESS: + tip = Math.toRadians(20.0); + base = Math.toRadians(90.0); + points[1*2] = (float) (points[2*3] - length*Math.cos(alpha)); + points[1*2+1] = (float) (points[2*3+1] - length*Math.sin(alpha)); + SL = length*Math.sin(base)/Math.sin(base+tip);; + break; + case NOTCHED: + tip = Math.toRadians(20); + base = Math.toRadians(120); + points[1*2] = (float) (points[2*3] - length*Math.cos(alpha)); + points[1*2+1] = (float) (points[2*3+1] - length*Math.sin(alpha)); + SL = length*Math.sin(base)/Math.sin(base+tip);; + break; + case OPEN: + tip = Math.toRadians(25); //30 + points[1*2] = points[2*3]; + points[1*2+1] = points[2*3+1]; + SL = length; + break; + case BAR: + tip = Math.toRadians(90); //30 + points[1*2] = points[2*3]; + points[1*2+1] = points[2*3+1]; + SL = length; + updateFullWindow = true; + break; + } + // P2 = P3 - SL*alpha+tip + points[2*2] = (float) (points[2*3] - SL*Math.cos(alpha+tip)); + points[2*2+1] = (float) (points[2*3+1] - SL*Math.sin(alpha+tip)); + // P4 = P3 - SL*alpha-tip + points[2*4] = (float) (points[2*3] - SL*Math.cos(alpha-tip)); + points[2*4+1] = (float) (points[2*3+1] - SL*Math.sin(alpha-tip)); + } + + private Shape getShape() { + Shape arrow = getPath(); + BasicStroke stroke = new BasicStroke((float)getStrokeWidth(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + Shape outlineShape = stroke.createStrokedShape(arrow); + Area a1 = new Area(arrow); + Area a2 = new Area(outlineShape); + try {a1.add(a2);} catch(Exception e) {}; + return a1; + } + + private ShapeRoi getShapeRoi() { + Shape arrow = getPath(); + BasicStroke stroke = new BasicStroke(getStrokeWidth(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + ShapeRoi sroi = new ShapeRoi(arrow); + Shape outlineShape = stroke.createStrokedShape(arrow); + sroi.or(new ShapeRoi(outlineShape)); + return sroi; + } + + public ImageProcessor getMask() { + if (width==0 && height==0) + return null; + else + return getShapeRoi().getMask(); + } + + private double getOutlineWidth() { + double width = getStrokeWidth()/8.0; + double head = headSize/7.0; + double lineWidth = width + head + headShaftRatio; + if (lineWidth<1.0) lineWidth = 1.0; + //if (width<1) width=1; + //if (head<1) head=1; + //IJ.log(getStrokeWidth()+" "+IJ.d2s(width,2)+" "+IJ.d2s(head,2)+" "+IJ.d2s(headShaftRatio,2)+" "+IJ.d2s(lineWidth,2)+" "+IJ.d2s(width*head,2)); + return lineWidth; + } + + public void drawPixels(ImageProcessor ip) { + ShapeRoi shapeRoi = getShapeRoi(); + ShapeRoi shapeRoi2 = null; + if (doubleHeaded) { + flipEnds(); + shapeRoi2 = getShapeRoi(); + flipEnds(); + } + if (outline) { + int lineWidth = ip.getLineWidth(); + ip.setLineWidth((int)Math.round(getOutlineWidth())); + shapeRoi.drawPixels(ip); + if (doubleHeaded) shapeRoi2.drawPixels(ip); + ip.setLineWidth(lineWidth); + } else { + ip.fill(shapeRoi); + if (doubleHeaded) ip.fill(shapeRoi2); + } + } + + public boolean contains(int x, int y) { + return getShapeRoi().contains(x, y); + } + + /** Return the bounding rectangle of this arrow. */ + public Rectangle getBounds() { + return getShapeRoi().getBounds(); + } + + protected void handleMouseDown(int sx, int sy) { + super.handleMouseDown(sx, sy); + startxd = ic!=null?ic.offScreenXD(sx):sx; + startyd = ic!=null?ic.offScreenYD(sy):sy; + } + + protected int clipRectMargin() { + double mag = getMagnification(); + double arrowWidth = getStrokeWidth(); + double size = 8+10*arrowWidth*mag*0.5; + return (int)Math.max(size*2.0, headSize); + } + + public boolean isDrawingTool() { + return true; + } + + public static void setDefaultWidth(double width) { + defaultWidth = (float)width; + } + + public static double getDefaultWidth() { + return defaultWidth; + } + + public void setStyle(int style) { + this.style = style; + } + + /* Set the style, where 'style' is "filled", "notched", "open", "headless" or "bar", + plus optionial modifiers of "outline", "double", "small", "medium" and "large". */ + public void setStyle(String style) { + style = style.toLowerCase(); + int newStyle = Arrow.FILLED; + if (style.contains("notched")) + newStyle = Arrow.NOTCHED; + else if (style.contains("open")) + newStyle = Arrow.OPEN; + else if (style.contains("headless")) + newStyle = Arrow.HEADLESS; + else if (style.contains("bar")) + newStyle = Arrow.BAR; + setStyle(newStyle); + setOutline(style.contains("outline")); + setDoubleHeaded(style.contains("double")); + if (style.contains("small")) + setHeadSize(5); + else if (style.contains("large")) + setHeadSize(15); + } + + public int getStyle() { + return style; + } + + public static void setDefaultStyle(int style) { + defaultStyle = style; + } + + public static int getDefaultStyle() { + return defaultStyle; + } + + public void setHeadSize(double headSize) { + this.headSize = headSize; + } + + public double getHeadSize() { + return headSize; + } + + public static void setDefaultHeadSize(double size) { + defaultHeadSize = size; + } + + public static double getDefaultHeadSize() { + return defaultHeadSize; + } + + public void setDoubleHeaded(boolean b) { + doubleHeaded = b; + } + + public boolean getDoubleHeaded() { + return doubleHeaded; + } + + public static void setDefaultDoubleHeaded(boolean b) { + defaultDoubleHeaded = b; + } + + public static boolean getDefaultDoubleHeaded() { + return defaultDoubleHeaded; + } + + public void setOutline(boolean b) { + outline = b; + } + + public boolean getOutline() { + return outline; + } + + public static void setDefaultOutline(boolean b) { + defaultOutline = b; + } + + public static boolean getDefaultOutline() { + return defaultOutline; + } + +} diff --git a/src/ij/gui/ColorChooser.java b/src/ij/gui/ColorChooser.java new file mode 100644 index 0000000..d5764bf --- /dev/null +++ b/src/ij/gui/ColorChooser.java @@ -0,0 +1,121 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.util.*; +import ij.plugin.Colors; +import java.awt.*; +import java.util.Vector; +import java.awt.event.*; + + + /** Displays a dialog that allows the user to select a color using three sliders. */ +public class ColorChooser implements TextListener, AdjustmentListener { + Vector colors, sliders; + ColorPanel panel; + Color initialColor; + int red, green, blue; + boolean useHSB; + String title; + Frame frame; + double scale = Prefs.getGuiScale(); + + /** Constructs a ColorChooser using the specified title and initial color. */ + public ColorChooser(String title, Color initialColor, boolean useHSB) { + this(title, initialColor, useHSB, null); + } + + public ColorChooser(String title, Color initialColor, boolean useHSB, Frame frame) { + this.title = title; + if (initialColor==null) initialColor = Color.black; + this.initialColor = initialColor; + red = initialColor.getRed(); + green = initialColor.getGreen(); + blue = initialColor.getBlue(); + this.useHSB = useHSB; + this.frame = frame; + } + + /** Displays a color selection dialog and returns the color selected by the user. */ + public Color getColor() { + GenericDialog gd = frame!=null?new GenericDialog(title, frame):new GenericDialog(title); + gd.addSlider("Red:", 0, 255, red); + gd.addSlider("Green:", 0, 255, green); + gd.addSlider("Blue:", 0, 255, blue); + panel = new ColorPanel(initialColor, scale); + gd.addPanel(panel, GridBagConstraints.CENTER, new Insets(10, 0, 0, 0)); + colors = gd.getNumericFields(); + for (int i=0; i255) red=255; + if (green<0) green=0; if (green>255) green=255; + if (blue<0) blue=0; if (blue>255) blue=255; + panel.setColor(new Color(red, green, blue)); + panel.repaint(); + } + + public synchronized void adjustmentValueChanged(AdjustmentEvent e) { + Object source = e.getSource(); + for (int i=0; ie is null if the + * dialogItemChanged method is called after the user has pressed the + * OK button or if the GenericDialog has read its parameters from a + * macro. + * @param gd A reference to the GenericDialog. + * @return Should be true if the dialog input is valid. False disables the + * OK button and preview (if any). + */ + boolean dialogItemChanged(GenericDialog gd, AWTEvent e); +} diff --git a/src/ij/gui/EllipseRoi.java b/src/ij/gui/EllipseRoi.java new file mode 100644 index 0000000..9796a32 --- /dev/null +++ b/src/ij/gui/EllipseRoi.java @@ -0,0 +1,269 @@ +package ij.gui; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; +import ij.*; +import ij.plugin.frame.Recorder; +import ij.process.FloatPolygon; +import ij.measure.Calibration; + +/** This class implements the ellipse selection tool. */ +public class EllipseRoi extends PolygonRoi { + private static final int vertices = 72; + private static double defaultRatio = 0.6; + private double xstart, ystart; + private double aspectRatio = defaultRatio; + private int[] handle = {0, vertices/4, vertices/2, vertices/2+vertices/4}; + + public EllipseRoi(double x1, double y1, double x2, double y2, double aspectRatio) { + super(new float[vertices], new float[vertices], vertices, FREEROI); + if (aspectRatio<0.0) aspectRatio = 0.0; + if (aspectRatio>1.0) aspectRatio = 1.0; + this.aspectRatio = aspectRatio; + makeEllipse(x1, y1, x2, y2); + state = NORMAL; + bounds = null; + } + + public EllipseRoi(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + type = FREEROI; + xstart = offScreenXD(sx); + ystart = offScreenYD(sy); + setDrawOffset(false); + bounds = null; + } + + public void draw(Graphics g) { + super.draw(g); + if (!overlay) { + for (int i=0; i1.0) aspectRatio = 1.0; + defaultRatio = aspectRatio; + } + + public int isHandle(int sx, int sy) { + int size = getHandleSize()+5; + int halfSize = size/2; + int index = -1; + for (int i=0; i=sx2 && sx<=sx2+size && sy>=sy2 && sy<=sy2+size) { + index = i; + break; + } + } + return index; + } + + /** Returns the perimeter of this ellipse. */ + public double getLength() { + double length = 0.0; + double dx, dy; + double w2=1.0, h2=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + w2 = cal.pixelWidth*cal.pixelWidth; + h2 = cal.pixelHeight*cal.pixelHeight; + } + for (int i=0; i<(nPoints-1); i++) { + dx = xpf[i+1]-xpf[i]; + dy = ypf[i+1]-ypf[i]; + length += Math.sqrt(dx*dx*w2+dy*dy*h2); + } + dx = xpf[0]-xpf[nPoints-1]; + dy = ypf[0]-ypf[nPoints-1]; + length += Math.sqrt(dx*dx*w2+dy*dy*h2); + return length; + } + + /** Returns x1, y1, x2, y2 and aspectRatio as a 5 element array. */ + public double[] getParams() { + double[] params = new double[5]; + params[0] = xpf[handle[2]]+x; + params[1] = ypf[handle[2]]+y; + params[2] = xpf[handle[0]]+x; + params[3] = ypf[handle[0]]+y; + params[4] = aspectRatio; + return params; + } + + public double[] getFeretValues() { + double a[] = super.getFeretValues(); + double pw=1.0, ph=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } + if (pw != ph) //the following calculation holds only for pixel aspect ratio == 1 (otherwise different axes in distorted ellipse) + return a; + double[] p = getParams(); + double dx = p[2] - p[0]; //this is always major axis; aspect ratio p[4] is limited to <= 1 + double dy = p[3] - p[1]; + double major = Math.sqrt(dx*dx + dy*dy); + double minor = major*p[4]; + a[0] = major*pw; //Feret from convex hull should be accurate anyhow + a[2] = minor*pw; //here our own calculation is better + System.arraycopy(p, 0, a, 8, 4); //MaxFeret endpoints + double xCenter = 0.5*(p[2] + p[0]); + double yCenter = 0.5*(p[3] + p[1]); + double semiMinorX = dy * 0.5 * p[4]; + double semiMinorY = dx * (-0.5) * p[4]; + a[12] = xCenter + semiMinorX; a[14] = xCenter - semiMinorX; + a[13] = yCenter + semiMinorY; a[15] = yCenter - semiMinorY; + return a; + } + + /** Always returns true. */ + public boolean subPixelResolution() { + return true; + } + +} diff --git a/src/ij/gui/FreehandRoi.java b/src/ij/gui/FreehandRoi.java new file mode 100644 index 0000000..db844b2 --- /dev/null +++ b/src/ij/gui/FreehandRoi.java @@ -0,0 +1,98 @@ +package ij.gui; + +import java.awt.*; +import java.awt.image.*; +import ij.*; + +/** Freehand region of interest or freehand line of interest*/ +public class FreehandRoi extends PolygonRoi { + + public FreehandRoi(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + if (Toolbar.getToolId()==Toolbar.FREEROI) + type = FREEROI; + else + type = FREELINE; + if (nPoints==2) nPoints--; + } + + protected void grow(int sx, int sy) { + if (subPixelResolution() && xpf!=null) { + growFloat(sx, sy); + return; + } + int ox = offScreenX(sx); + int oy = offScreenY(sy); + if (ox<0) ox = 0; + if (oy<0) oy = 0; + if (ox>xMax) ox = xMax; + if (oy>yMax) oy = yMax; + if (ox!=xp[nPoints-1]+x || oy!=yp[nPoints-1]+y) { + xp[nPoints] = ox-x; + yp[nPoints] = oy-y; + nPoints++; + if (IJ.altKeyDown()) + wipeBack(); + if (nPoints==xp.length) + enlargeArrays(); + drawLine(); + } + } + + private void growFloat(int sx, int sy) { + double ox = offScreenXD(sx); + double oy = offScreenYD(sy); + if (ox<0.0) ox = 0.0; + if (oy<0.0) oy = 0.0; + if (ox>xMax) ox = xMax; + if (oy>yMax) oy = yMax; + double xbase = getXBase(); + double ybase = getYBase(); + if (ox!=xpf[nPoints-1]+xbase || oy!=ypf[nPoints-1]+ybase) { + xpf[nPoints] = (float)(ox-xbase); + ypf[nPoints] = (float)(oy-ybase); + nPoints++; + if (nPoints==xpf.length) + enlargeArrays(); + drawLine(); + } + } + + void drawLine() { + int x1, y1, x2, y2; + if (xpf!=null) { + x1 = (int)Math.round(xpf[nPoints-2]+x); + y1 = (int)Math.round(ypf[nPoints-2]+y); + x2 = (int)Math.round(xpf[nPoints-1]+x); + y2 = (int)Math.round(ypf[nPoints-1]+y); + } else { + x1 = xp[nPoints-2]+x; + y1 = yp[nPoints-2]+y; + x2 = xp[nPoints-1]+x; + y2 = yp[nPoints-1]+y; + } + int xmin = Math.min(x1, x2); + int xmax = Math.max(x1, x2); + int ymin = Math.min(y1, y2); + int ymax = Math.max(y1, y2); + int margin = 4; + if (lineWidth>margin && isLine()) + margin = lineWidth; + if (ic!=null) { + double mag = ic.getMagnification(); + if (mag<1.0) margin = (int)(margin/mag); + } + if (IJ.altKeyDown()) + margin += 20; // for wipeBack + imp.draw(xmin-margin, ymin-margin, (xmax-xmin)+margin*2, (ymax-ymin)+margin*2); + } + + protected void handleMouseUp(int screenX, int screenY) { + if (state==CONSTRUCTING) { + addOffset(); + finishPolygon(); + } + state = NORMAL; + } + +} diff --git a/src/ij/gui/GUI.java b/src/ij/gui/GUI.java new file mode 100644 index 0000000..6bb4921 --- /dev/null +++ b/src/ij/gui/GUI.java @@ -0,0 +1,288 @@ +package ij.gui; +import ij.*; +import java.awt.*; +import javax.swing.JComponent; +import javax.swing.JList; +import javax.swing.JTable; +import javax.swing.UIManager; + +/** This class consists of static GUI utility methods. */ +public class GUI { + private static final Font DEFAULT_FONT = IJ.font12; + private static Color lightGray = new Color(240,240,240); + private static boolean isWindows8; + private static Color scrollbarBackground = new Color(245,245,245); + + static { + if (IJ.isWindows()) { + String osname = System.getProperty("os.name"); + isWindows8 = osname.contains("unknown") || osname.contains("8"); + } + } + + /** Positions the specified window in the center of the screen that contains target. */ + public static void center(Window win, Component target) { + if (win == null) + return; + Rectangle bounds = getMaxWindowBounds(target); + Dimension window = win.getSize(); + if (window.width == 0) + return; + int left = bounds.x + Math.max(0, (bounds.width - window.width) / 2); + int top = bounds.y + Math.max(0, (bounds.height - window.height) / 4); + win.setLocation(left, top); + } + + /** Positions the specified window in the center of the + screen containing the "ImageJ" window. */ + public static void centerOnImageJScreen(Window win) { + center(win, IJ.getInstance()); + } + + public static void center(Window win) { + center(win, win); + } + + private static java.util.List getScreenConfigs() { + java.util.ArrayList configs = new java.util.ArrayList(); + for (GraphicsDevice device : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { + configs.add(device.getDefaultConfiguration()); + } + return configs; + } + + /** + * Get maximum bounds for the screen that contains a given point. + * @param point Coordinates of point. + * @param accountForInsets Deduct the space taken up by menu and status bars, etc. (after point is found to be inside bonds) + * @return Rectangle of bounds or null if point not inside of any screen. + */ + public static Rectangle getScreenBounds(Point point, boolean accountForInsets) { + if (GraphicsEnvironment.isHeadless()) + return new Rectangle(0,0,0,0); + for (GraphicsConfiguration config : getScreenConfigs()) { + Rectangle bounds = config.getBounds(); + if (bounds != null && bounds.contains(point)) { + Insets insets = accountForInsets ? Toolkit.getDefaultToolkit().getScreenInsets(config) : null; + return shrinkByInsets(bounds, insets); + } + } + return null; + } + + /** + * Get maximum bounds for the screen that contains a given component. + * @param component An AWT component located on the desired screen. + * If null is provided, the default screen is used. + * @param accountForInsets Deduct the space taken up by menu and status bars, etc. + * @return Rectangle of bounds. + */ + public static Rectangle getScreenBounds(Component component, boolean accountForInsets) { + if (GraphicsEnvironment.isHeadless()) + return new Rectangle(0,0,0,0); + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsConfiguration gc = component == null ? ge.getDefaultScreenDevice().getDefaultConfiguration() : + component.getGraphicsConfiguration(); + Insets insets = accountForInsets ? Toolkit.getDefaultToolkit().getScreenInsets(gc) : null; + return shrinkByInsets(gc.getBounds(), insets); + } + + public static Rectangle getScreenBounds(Point point) { + return getScreenBounds(point, false); + } + + public static Rectangle getScreenBounds(Component component) { + return getScreenBounds(component, false); + } + + public static Rectangle getScreenBounds() { + return getScreenBounds((Component)null); + } + + public static Rectangle getMaxWindowBounds(Point point) { + return getScreenBounds(point, true); + } + + public static Rectangle getMaxWindowBounds(Component component) { + return getScreenBounds(component, true); + } + + public static Rectangle getMaxWindowBounds() { + return getMaxWindowBounds((Component)null); + } + + private static Rectangle shrinkByInsets(Rectangle bounds, Insets insets) { + Rectangle shrunk = new Rectangle(bounds); + if (insets == null) return shrunk; + shrunk.x += insets.left; + shrunk.y += insets.top; + shrunk.width -= insets.left + insets.right; + shrunk.height -= insets.top + insets.bottom; + return shrunk; + } + + public static Rectangle getZeroBasedMaxBounds() { + for (GraphicsConfiguration config : getScreenConfigs()) { + Rectangle bounds = config.getBounds(); + if (bounds != null && bounds.x == 0 && bounds.y == 0) + return bounds; + } + return null; + } + + public static Rectangle getUnionOfBounds() { + Rectangle unionOfBounds = new Rectangle(); + for (GraphicsConfiguration config : getScreenConfigs()) { + unionOfBounds = unionOfBounds.union(config.getBounds()); + } + return unionOfBounds; + } + + static private Frame frame; + + /** Obsolete */ + public static Image createBlankImage(int width, int height) { + if (width==0 || height==0) + throw new IllegalArgumentException(""); + if (frame==null) { + frame = new Frame(); + frame.pack(); + frame.setBackground(Color.white); + } + Image img = frame.createImage(width, height); + return img; + } + + /** Lightens overly dark scrollbar background on Windows 8. */ + public static void fix(Scrollbar sb) { + } + + public static boolean showCompositeAdvisory(ImagePlus imp, String title) { + if (imp==null || imp.getCompositeMode()!=IJ.COMPOSITE || imp.getNChannels()==1 || IJ.macroRunning()) + return true; + String msg = "Channel "+imp.getC()+" of this color composite image will be processed."; + GenericDialog gd = new GenericDialog(title); + gd.addMessage(msg); + gd.showDialog(); + return !gd.wasCanceled(); + } + + /** + * Scales an AWT component according to {@link Prefs#getGuiScale()}. + * @param component the AWT component to be scaled. If a container, scaling is applied to all its child components + */ + public static void scale(final Component component) { + final float scale = (float)Prefs.getGuiScale(); + if (scale==1f) + return; + if (component instanceof Container) + scaleComponents((Container)component, scale); + else + scaleComponent(component, scale); + } + + private static void scaleComponents(final Container container, final float scale) { + for (final Component child : container.getComponents()) { + if (child instanceof Container) + scaleComponents((Container) child, scale); + else + scaleComponent(child, scale); + } + } + + private static void scaleComponent(final Component component, final float scale) { + Font font = component.getFont(); + if (font == null) + font = DEFAULT_FONT; + font = font.deriveFont(scale*font.getSize()); + component.setFont(font); + } + + public static void scalePopupMenu(final PopupMenu popup) { + final float scale = (float)Prefs.getGuiScale(); + if (scale==1f) + return; + Font font = popup.getFont(); + if (font == null) + font = DEFAULT_FONT; + font = font.deriveFont(scale*font.getSize()); + popup.setFont(font); + } + + /** + * Tries to detect if a Swing component is unscaled and scales it it according + * to {@link #getGuiScale()}. + *

+ * This is mainly relevant to linux: Swing components scale automatically on + * most platforms, specially since Java 8. However there are still exceptions to + * this on linux: e.g., In Ubuntu, Swing components do scale, but only under the + * GTK L&F. (On the other hand AWT components do not scale at all on + * hiDPI screens on linux). + *

+ *

+ * This method tries to avoid exaggerated font sizes by detecting if a component + * has been already scaled by the UIManager, applying only + * {@link #getGuiScale()} to the component's font if not. + *

+ * + * @param component the component to be scaled + * @return true, if component's font was resized + */ + public static boolean scale(final JComponent component) { + final double guiScale = Prefs.getGuiScale(); + if (guiScale == 1d) + return false; + Font font = component.getFont(); + if (font == null && component instanceof JList) + font = UIManager.getFont("List.font"); + else if (font == null && component instanceof JTable) + font = UIManager.getFont("Table.font"); + else if (font == null) + font = UIManager.getFont("Label.font"); + if (font.getSize() > DEFAULT_FONT.getSize()) + return false; + if (component instanceof JTable) + ((JTable) component).setRowHeight((int) (((JTable) component).getRowHeight() * guiScale * 0.9)); + else if (component instanceof JList) + ((JList) component).setFixedCellHeight((int) (((JList) component).getFixedCellHeight() * guiScale * 0.9)); + component.setFont(font.deriveFont((float) guiScale * font.getSize())); + return true; + } + + /** Works around an OpenJDK bug on Windows that + * causes the scrollbar thumb color and background + * color to be almost identical. + */ + public static final void fixScrollbar(Scrollbar sb) { + if (IJ.isWindows()) + sb.setBackground(scrollbarBackground); + } + + /** Returns a new NonBlockingGenericDialog with the given title, + * except when Java is running in headless mode, in which case + * a GenericDialog is be returned. + */ + public static GenericDialog newNonBlockingDialog(String title) { + if (GraphicsEnvironment.isHeadless()) + return new GenericDialog(title); + else + return new NonBlockingGenericDialog(title); + } + + /** Returns a new NonBlockingGenericDialog with the given title + * if Prefs.nonBlockingFilterDialogs is 'true' and 'imp' is + * displayed, otherwise returns a GenericDialog. + * @param title Dialog title + * @param imp The image associated with this dialog + */ + public static GenericDialog newNonBlockingDialog(String title, ImagePlus imp) { + if (Prefs.nonBlockingFilterDialogs && imp!=null && imp.getWindow()!=null) { + NonBlockingGenericDialog gd = new NonBlockingGenericDialog(title); + gd.imp = imp; + return gd; + } else + return new GenericDialog(title); + } + + +} diff --git a/src/ij/gui/GenericDialog.java b/src/ij/gui/GenericDialog.java new file mode 100644 index 0000000..0995d71 --- /dev/null +++ b/src/ij/gui/GenericDialog.java @@ -0,0 +1,1918 @@ +package ij.gui; +import ij.*; +import ij.plugin.frame.Recorder; +import ij.plugin.ScreenGrabber; +import ij.plugin.filter.PlugInFilter; +import ij.plugin.filter.PlugInFilterRunner; +import ij.util.Tools; +import ij.macro.*; +import ij.io.OpenDialog; +import java.awt.*; +import java.io.*; +import java.awt.event.*; +import java.util.*; +import java.awt.datatransfer.*; +import java.awt.dnd.*; + + +/** + * This class is a customizable modal dialog box. Here is an example + * GenericDialog with one string field and two numeric fields: + *
+ *  public class Generic_Dialog_Example implements PlugIn {
+ *    static String title="Example";
+ *    static int width=512,height=512;
+ *    public void run(String arg) {
+ *      GenericDialog gd = new GenericDialog("New Image");
+ *      gd.addStringField("Title: ", title);
+ *      gd.addNumericField("Width: ", width, 0);
+ *      gd.addNumericField("Height: ", height, 0);
+ *      gd.showDialog();
+ *      if (gd.wasCanceled()) return;
+ *      title = gd.getNextString();
+ *      width = (int)gd.getNextNumber();
+ *      height = (int)gd.getNextNumber();
+ *      IJ.newImage(title, "8-bit", width, height, 1);
+ *   }
+ * }
+ * 
+* To work with macros, the first word of each component label must be +* unique. If this is not the case, add underscores, which will be converted +* to spaces when the dialog is displayed. For example, change the checkbox labels +* "Show Quality" and "Show Residue" to "Show_Quality" and "Show_Residue". +*/ +public class GenericDialog extends Dialog implements ActionListener, TextListener, +FocusListener, ItemListener, KeyListener, AdjustmentListener, WindowListener { + + protected Vector numberField, stringField, checkbox, choice, slider, radioButtonGroups; + protected TextArea textArea1, textArea2; + protected Vector defaultValues,defaultText,defaultStrings,defaultChoiceIndexes; + protected Component theLabel; + private Button okay; + private Button cancel; + private Button no, help; + private String helpLabel = "Help"; + private boolean wasCanceled, wasOKed; + private int nfIndex, sfIndex, cbIndex, choiceIndex, textAreaIndex, radioButtonIndex; + private GridBagConstraints c; + private boolean firstNumericField=true; + private boolean firstSlider=true; + private boolean invalidNumber; + private String errorMessage; + private Hashtable labels; + private boolean macro; + private String macroOptions; + private boolean addToSameRow; + private boolean addToSameRowCalled; + private int topInset, leftInset, bottomInset; + private boolean customInsets; + private Vector sliderIndexes, sliderScales, sliderDigits; + private Checkbox previewCheckbox; // the "Preview" Checkbox, if any + private Vector dialogListeners; // the Objects to notify on user input + private PlugInFilterRunner pfr; // the PlugInFilterRunner for automatic preview + private String previewLabel = " Preview"; + private final static String previewRunning = "wait..."; + private boolean recorderOn; // whether recording is allowed (after the dialog is closed) + private char echoChar; + private boolean hideCancelButton; + private boolean centerDialog = true; + private String helpURL; + private boolean smartRecording; + private Vector imagePanels; + protected static GenericDialog instance; + private boolean firstPaint = true; + private boolean fontSizeSet; + private boolean showDialogCalled; + private boolean optionsRecorded; // have dialogListeners been called to record options? + private Label lastLabelAdded; + private int[] windowIDs; + private String[] windowTitles; + + + /** Creates a new GenericDialog with the specified title. Uses the current image + image window as the parent frame or the ImageJ frame if no image windows + are open. Dialog parameters are recorded by ImageJ's command recorder but + this requires that the first word of each label be unique. */ + public GenericDialog(String title) { + this(title, null); + } + + private static Frame getParentFrame() { + return null; + } + + /** Creates a new GenericDialog using the specified title and parent frame. */ + public GenericDialog(String title, Frame parent) { + super(parent, title, true); + ImageJ ij = IJ.getInstance(); + if (ij!=null) setFont(ij.getFont()); + okay = new Button(" OK "); + cancel = new Button("Cancel"); + if (Prefs.blackCanvas) { + setForeground(SystemColor.controlText); + setBackground(SystemColor.control); + } + //if (IJ.isMacOSX() && System.getProperty("java.vendor").contains("Azul")) + // setForeground(Color.black); // work around bug on Azul Java 8 on Apple Silicon + GridBagLayout grid = new GridBagLayout(); + c = new GridBagConstraints(); + setLayout(grid); + macroOptions = Macro.getOptions(); + macro = macroOptions!=null; + addKeyListener(this); + addWindowListener(this); + } + + /** Adds a numeric field. The first word of the label must be + unique or command recording will not work. + * @param label the label + * @param defaultValue value to be initially displayed + */ + public void addNumericField(String label, double defaultValue) { + int decimalPlaces = (int)defaultValue==defaultValue?0:3; + int columnWidth = decimalPlaces==3?8:6; + addNumericField(label, defaultValue, decimalPlaces, columnWidth, null); + } + + /** Adds a numeric field. The first word of the label must be + unique or command recording will not work. + * @param label the label + * @param defaultValue value to be initially displayed + * @param digits number of digits to right of decimal point + */ + public void addNumericField(String label, double defaultValue, int digits) { + addNumericField(label, defaultValue, digits, 6, null); + } + + /** Adds a numeric field. The first word of the label must be + unique or command recording will not work. + * @param label the label + * @param defaultValue value to be initially displayed + * @param digits number of digits to right of decimal point + * @param columns width of field in characters + * @param units a string displayed to the right of the field + */ + public void addNumericField(String label, double defaultValue, int digits, int columns, String units) { + String label2 = label; + if (label2.indexOf('_')!=-1) + label2 = label2.replace('_', ' '); + Label fieldLabel = makeLabel(label2); + this.lastLabelAdded = fieldLabel; + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + c.insets.left = 10; + } else { + c.gridx = 0; c.gridy++; + if (firstNumericField) + c.insets = getInsets(5, 0, 3, 0); // top, left, bottom, right + else + c.insets = getInsets(0, 0, 3, 0); + } + c.anchor = GridBagConstraints.EAST; + c.gridwidth = 1; + //IJ.log("x="+c.gridx+", y= "+c.gridy+", width="+c.gridwidth+", ancher= "+c.anchor+" "+c.insets); + add(fieldLabel, c); + if (addToSameRow) { + c.insets.left = 0; + addToSameRow = false; + } + if (numberField==null) { + numberField = new Vector(5); + defaultValues = new Vector(5); + defaultText = new Vector(5); + } + if (IJ.isWindows()) columns -= 2; + if (columns<1) columns = 1; + String defaultString = IJ.d2s(defaultValue, digits); + if (Double.isNaN(defaultValue)) + defaultString = ""; + TextField tf = new TextField(defaultString, columns); + if (IJ.isLinux()) tf.setBackground(Color.white); + tf.addActionListener(this); + tf.addTextListener(this); + tf.addFocusListener(this); + tf.addKeyListener(this); + numberField.addElement(tf); + defaultValues.addElement(new Double(defaultValue)); + defaultText.addElement(tf.getText()); + c.gridx = GridBagConstraints.RELATIVE; + c.anchor = GridBagConstraints.WEST; + tf.setEditable(true); + //if (firstNumericField) tf.selectAll(); + firstNumericField = false; + if (units==null||units.equals("")) { + add(tf, c); + } else { + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + panel.add(tf); + panel.add(new Label(" "+units)); + add(panel, c); + } + if (Recorder.record || macro) + saveLabel(tf, label); + } + + private Label makeLabel(String label) { + if (IJ.isMacintosh()) + label += " "; + return new Label(label); + } + + /** Saves the label for given component, for macro recording and for accessing the component in macros. */ + private void saveLabel(Object component, String label) { + if (labels==null) + labels = new Hashtable(); + if (label.length()>0) + label = Macro.trimKey(label.trim()); + if (label.length()>0 && hasLabel(label)) { // not a unique label? + label += "_0"; + for (int n=1; hasLabel(label); n++) { // while still not a unique label + label = label.substring(0, label.lastIndexOf('_')); //remove counter + label += "_"+n; + } + } + labels.put(component, label); + } + + /** Returns whether the list of labels for macro recording or macro creation contains a given label. */ + private boolean hasLabel(String label) { + for (Object o : labels.keySet()) + if (labels.get(o).equals(label)) return true; + return false; + } + + /** Adds an 8 column text field. + * @param label the label + * @param defaultText the text initially displayed + */ + public void addStringField(String label, String defaultText) { + addStringField(label, defaultText, 8); + } + + /** Adds a text field. + * @param label the label + * @param defaultText text initially displayed + * @param columns width of the text field. If columns is 8 or more, additional items may be added to this line with addToSameRow() + */ + public void addStringField(String label, String defaultText, int columns) { + if (addToSameRow && label.equals("_")) + label = ""; + String label2 = label; + if (label2.indexOf('_')!=-1) + label2 = label2.replace('_', ' '); + Label fieldLabel = makeLabel(label2); + this.lastLabelAdded = fieldLabel; + boolean custom = customInsets; + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + addToSameRow = false; + } else { + c.gridx = 0; c.gridy++; + if (stringField==null) + c.insets = getInsets(5, 0, 5, 0); // top, left, bottom, right + else + c.insets = getInsets(0, 0, 5, 0); + } + c.anchor = GridBagConstraints.EAST; + c.gridwidth = 1; + add(fieldLabel, c); + if (stringField==null) { + stringField = new Vector(4); + defaultStrings = new Vector(4); + } + + TextField tf = new TextField(defaultText, columns); + if (IJ.isLinux()) tf.setBackground(Color.white); + tf.setEchoChar(echoChar); + echoChar = 0; + tf.addActionListener(this); + tf.addTextListener(this); + tf.addFocusListener(this); + tf.addKeyListener(this); + c.gridx = GridBagConstraints.RELATIVE; + c.anchor = GridBagConstraints.WEST; + c.gridwidth = columns <= 8 ? 1 : GridBagConstraints.REMAINDER; + c.insets.left = 0; + tf.setEditable(true); + add(tf, c); + stringField.addElement(tf); + defaultStrings.addElement(defaultText); + tf.setDropTarget(null); + new DropTarget(tf, new TextDropTarget(tf)); + if (Recorder.record || macro) + saveLabel(tf, label); + } + + /** Sets the echo character for the next string field. */ + public void setEchoChar(char echoChar) { + this.echoChar = echoChar; + } + + /** Adds a directory text field and "Browse" button, where the + * field width is determined by the length of 'defaultPath', with + * a minimum of 25 columns. Use getNextString to retrieve the + * directory path. Based on the addDirectoryField() method in + * Fiji's GenericDialogPlus class. + */ + public void addDirectoryField(String label, String defaultPath) { + int columns = defaultPath!=null?Math.max(defaultPath.length(),25):25; + addDirectoryField(label, defaultPath, columns); + } + + public void addDirectoryField(String label, String defaultPath, int columns) { + defaultPath = IJ.addSeparator(defaultPath); + addStringField(label, defaultPath, columns); + if (GraphicsEnvironment.isHeadless()) + return; + TextField text = (TextField)stringField.lastElement(); + GridBagLayout layout = (GridBagLayout)getLayout(); + GridBagConstraints constraints = layout.getConstraints(text); + Button button = new TrimmedButton("Browse",IJ.isMacOSX()?10:0); + BrowseButtonListener listener = new BrowseButtonListener(label, text, "dir"); + button.addActionListener(listener); + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + panel.add(text); + panel.add(button); + layout.setConstraints(panel, constraints); + add(panel); + if (Recorder.record || macro) + saveLabel(panel, label); + } + + /** Adds a file text field and "Browse" button, where the + * field width is determined by the length of 'defaultPath', + * with a minimum of 25 columns. Use getNextString to + * retrieve the file path. Based on the addFileField() method + * in Fiji's GenericDialogPlus class. + */ + public void addFileField(String label, String defaultPath) { + int columns = defaultPath!=null?Math.max(defaultPath.length(),25):25; + addFileField(label, defaultPath, columns); + } + + public void addFileField(String label, String defaultPath, int columns) { + addStringField(label, defaultPath, columns); + if (GraphicsEnvironment.isHeadless()) + return; + TextField text = (TextField)stringField.lastElement(); + GridBagLayout layout = (GridBagLayout)getLayout(); + GridBagConstraints constraints = layout.getConstraints(text); + Button button = new TrimmedButton("Browse",IJ.isMacOSX()?10:0); + BrowseButtonListener listener = new BrowseButtonListener(label, text, "file"); + button.addActionListener(listener); + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + panel.add(text); + panel.add(button); + layout.setConstraints(panel, constraints); + add(panel); + if (Recorder.record || macro) + saveLabel(panel, label); + } + + /** + * Add button to the dialog + * @param label button label + * @param listener listener to handle the action when pressing the button + */ + public void addButton(String label, ActionListener listener) { + if (GraphicsEnvironment.isHeadless()) + return; + Button button = new Button(label); + button.addActionListener(listener); + button.addKeyListener(this); + GridBagLayout layout = (GridBagLayout)getLayout(); + Panel panel = new Panel(); + addPanel(panel); + GridBagConstraints constraints = layout.getConstraints(panel); + remove(panel); + layout.setConstraints(button, constraints); + add(button); + } + + /** Adds a popup menu that lists the currently open images. + * Call getNextImage() to retrieve the selected + * image. Based on the addImageChoice() + * method in Fiji's GenericDialogPlus class. + * @param label the label + * @param defaultImage the image title initially selected in the menu + * or the first image if null + */ + public void addImageChoice(String label, String defaultImage) { + if (windowTitles==null) { + windowIDs = WindowManager.getIDList(); + if (windowIDs==null) + windowIDs = new int[0]; + windowTitles = new String[windowIDs.length]; + for (int i=0; ienum class of the specified default item (enum constant). + * The default item is automatically set. Calls the original (string-based) + * {@link GenericDialog#addChoice(String, String[], String)} method. + * + * @param the generic enum type containing the items to chose from + * @param label the label displayed for this choice group + * @param defaultItem the menu item initially selected + */ + public > void addEnumChoice(String label, Enum defaultItem) { + Class enumClass = defaultItem.getDeclaringClass(); + E[] enums = enumClass.getEnumConstants(); + String[] items = new String[enums.length]; + for (int i = 0; i < enums.length; i++) { + items[i] = enums[i].name(); + } + this.addChoice(label, items, defaultItem.name()); + } + + /** + * Returns the selected item in the next enum choice menu. + * Note that 'enumClass' is required to infer the proper enum type. + * Throws {@code IllegalArgumentException} if the selected item is not a defined + * constant in the specified enum class. + * + * @param the generic enum type + * @param enumClass the enum type + * @return the selected item + */ + public > E getNextEnumChoice(Class enumClass) { + String choiceString = this.getNextChoice(); + return Enum.valueOf(enumClass, choiceString); + } + + /** Adds a checkbox. + * @param label the label + * @param defaultValue the initial state + */ + public void addCheckbox(String label, boolean defaultValue) { + addCheckbox(label, defaultValue, false); + } + + /** Adds a checkbox; does not make it recordable if isPreview is true. + * With isPreview true, the checkbox can be referred to as previewCheckbox + * from hereon. + */ + private void addCheckbox(String label, boolean defaultValue, boolean isPreview) { + String label2 = label; + if (label2.indexOf('_')!=-1) + label2 = label2.replace('_', ' '); + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + c.insets.left = 10; + addToSameRow = false; + } else { + c.gridx = 0; c.gridy++; + if (checkbox==null) + c.insets = getInsets(15, 20, 0, 0); // top, left, bottom, right + else + c.insets = getInsets(0, 20, 0, 0); + } + c.anchor = GridBagConstraints.WEST; + c.gridwidth = 2; + if (checkbox==null) + checkbox = new Vector(4); + Checkbox cb = new Checkbox(label2); + cb.setState(defaultValue); + cb.addItemListener(this); + cb.addKeyListener(this); + add(cb, c); + c.insets.left = 0; + checkbox.addElement(cb); + if (!isPreview &&(Recorder.record || macro)) //preview checkbox is not recordable + saveLabel(cb, label); + if (isPreview) previewCheckbox = cb; + } + + /** Adds a checkbox labelled "Preview" for "automatic" preview. + * The reference to this checkbox can be retrieved by getPreviewCheckbox() + * and it provides the additional method previewRunning for optical + * feedback while preview is prepared. + * PlugInFilters can have their "run" method automatically called for + * preview under the following conditions: + * - the PlugInFilter must pass a reference to itself (i.e., "this") as an + * argument to the AddPreviewCheckbox + * - it must implement the DialogListener interface and set the filter + * parameters in the dialogItemChanged method. + * - it must have DIALOG and PREVIEW set in its flags. + * A previewCheckbox is always off when the filter is started and does not get + * recorded by the Macro Recorder. + * + * @param pfr A reference to the PlugInFilterRunner calling the PlugInFilter + * if automatic preview is desired, null otherwise. + */ + public void addPreviewCheckbox(PlugInFilterRunner pfr) { + if (previewCheckbox != null) + return; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null && imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.COMPOSITE) + return; + this.pfr = pfr; + addCheckbox(previewLabel, false, true); + } + + /** Add the preview checkbox with user-defined label; for details see the + * addPreviewCheckbox method with standard "Preview" label. + * Adds the checkbox when the current image is a CompositeImage + * in "Composite" mode, unlike the one argument version. + * Note that a GenericDialog can have only one PreviewCheckbox. + */ + public void addPreviewCheckbox(PlugInFilterRunner pfr, String label) { + if (previewCheckbox!=null) + return; + previewLabel = label; + this.pfr = pfr; + addCheckbox(previewLabel, false, true); + } + + /** Adds a group of checkboxs using a grid layout. + * @param rows the number of rows + * @param columns the number of columns + * @param labels the labels + * @param defaultValues the initial states + */ + public void addCheckboxGroup(int rows, int columns, String[] labels, boolean[] defaultValues) { + addCheckboxGroup(rows, columns, labels, defaultValues, null); + } + + /** Adds a group of checkboxs using a grid layout. + * @param rows the number of rows + * @param columns the number of columns + * @param labels the labels + * @param defaultValues the initial states + * @param headings the column headings + * Example: http://imagej.nih.gov/ij/plugins/multi-column-dialog/index.html + */ + public void addCheckboxGroup(int rows, int columns, String[] labels, boolean[] defaultValues, String[] headings) { + Panel panel = new Panel(); + int nRows = headings!=null?rows+1:rows; + panel.setLayout(new GridLayout(nRows, columns, 6, 0)); + int startCBIndex = cbIndex; + if (checkbox==null) + checkbox = new Vector(12); + if (headings!=null) { + Font font = new Font("SansSerif", Font.BOLD, 12); + for (int i=0; iheadings.length-1 || headings[i]==null) + panel.add(new Label("")); + else { + Label label = new Label(headings[i]); + label.setFont(font); + panel.add(label); + } + } + } + int i1 = 0; + int[] index = new int[labels.length]; + for (int row=0; row=labels.length) break; + index[i1] = i2; + String label = labels[i1]; + if (label==null || label.length()==0) { + Label lbl = new Label(""); + panel.add(lbl); + i1++; + continue; + } + if (label.indexOf('_')!=-1) + label = label.replace('_', ' '); + Checkbox cb = new Checkbox(label); + checkbox.addElement(cb); + cb.setState(defaultValues[i1]); + cb.addItemListener(this); + if (Recorder.record || macro) + saveLabel(cb, labels[i1]); + if (IJ.isLinux()) { + Panel panel2 = new Panel(); + panel2.setLayout(new BorderLayout()); + panel2.add("West", cb); + panel.add(panel2); + } else + panel.add(cb); + i1++; + } + } + c.gridx = 0; c.gridy++; + c.gridwidth = GridBagConstraints.REMAINDER; + c.anchor = GridBagConstraints.WEST; + c.insets = getInsets(10, 0, 0, 0); + addToSameRow = false; + add(panel, c); + } + + /** Adds a radio button group. + * @param label group label (or null) + * @param items radio button labels + * @param rows number of rows + * @param columns number of columns + * @param defaultItem button initially selected + */ + public void addRadioButtonGroup(String label, String[] items, int rows, int columns, String defaultItem) { + addToSameRow = false; + Panel panel = new Panel(); + int n = items.length; + panel.setLayout(new GridLayout(rows, columns, 0, 0)); + CheckboxGroup cg = new CheckboxGroup(); + for (int i=0; i=0) + theLabel = new MultiLineLabel(text); + else + theLabel = new Label(text); + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + addToSameRow = false; + } else { + c.gridx = 0; c.gridy++; + c.insets = getInsets("".equals(text)?0:10, 20, 0, 0); // top, left, bottom, right + } + c.gridwidth = GridBagConstraints.REMAINDER; + c.anchor = GridBagConstraints.WEST; + c.fill = GridBagConstraints.HORIZONTAL; + if (font!=null) { + if (Prefs.getGuiScale()>1.0) + font = font.deriveFont((float)(font.getSize()*Prefs.getGuiScale())); + theLabel.setFont(font); + } + if (color!=null) + theLabel.setForeground(color); + add(theLabel, c); + c.fill = GridBagConstraints.NONE; + } + + /** Adds one or two (side by side) text areas. + * Append "SCROLLBARS_VERTICAL_ONLY" to the text of + * the first text area to get vertical scrollbars + * and "SCROLLBARS_BOTH" to get both vertical and + * horizontal scrollbars. + * @param text1 initial contents of the first text area + * @param text2 initial contents of the second text area or null + * @param rows the number of rows + * @param columns the number of columns + */ + public void addTextAreas(String text1, String text2, int rows, int columns) { + if (textArea1!=null) return; + Panel panel = new Panel(); + int scrollbars = TextArea.SCROLLBARS_NONE; + if (text1!=null && text1.endsWith("SCROLLBARS_BOTH")) { + scrollbars = TextArea.SCROLLBARS_BOTH; + text1 = text1.substring(0, text1.length()-15); + } + if (text1!=null && text1.endsWith("SCROLLBARS_VERTICAL_ONLY")) { + scrollbars = TextArea.SCROLLBARS_VERTICAL_ONLY; + text1 = text1.substring(0, text1.length()-24); + } + Font font = new Font("SansSerif", Font.PLAIN, (int)(14*Prefs.getGuiScale())); + textArea1 = new TextArea(text1,rows,columns,scrollbars); + if (IJ.isLinux()) textArea1.setBackground(Color.white); + textArea1.setFont(font); + textArea1.addTextListener(this); + panel.add(textArea1); + if (text2!=null) { + textArea2 = new TextArea(text2,rows,columns,scrollbars); + if (IJ.isLinux()) textArea2.setBackground(Color.white); + textArea2.setFont(font); + panel.add(textArea2); + } + c.gridx = 0; c.gridy++; + c.gridwidth = GridBagConstraints.REMAINDER; + c.anchor = GridBagConstraints.WEST; + c.insets = getInsets(15, 20, 0, 0); + addToSameRow = false; + add(panel, c); + } + + /** + * Adds a slider (scroll bar) to the dialog box. + * Floating point values are used if (maxValue-minValue)<=5.0 + * and either defaultValue or minValue are non-integer. + * @param label the label + * @param minValue the minimum value of the slider + * @param maxValue the maximum value of the slider + * @param defaultValue the initial value of the slider + */ + public void addSlider(String label, double minValue, double maxValue, double defaultValue) { + if (defaultValuemaxValue) defaultValue=maxValue; + int digits = 0; + double scale = 1.0; + if ((maxValue-minValue)<=5.0 && (minValue!=(int)minValue||maxValue!=(int)maxValue||defaultValue!=(int)defaultValue)) { + scale = 50.0; + minValue *= scale; + maxValue *= scale; + defaultValue *= scale; + digits = 2; + } + addSlider( label, minValue, maxValue, defaultValue, scale, digits); + } + + /** This vesion of addSlider() adds a 'stepSize' argument.
+ * Example: http://wsr.imagej.net/macros/SliderDemo.txt + */ + public void addSlider(String label, double minValue, double maxValue, double defaultValue, double stepSize) { + if ( stepSize <= 0 ) stepSize = 1; + int digits = digits(stepSize); + if (digits==1 && "Angle:".equals(label)) + digits = 2; + double scale = 1.0 / Math.abs( stepSize ); + if ( scale <= 0 ) scale = 1; + if ( defaultValue < minValue ) defaultValue = minValue; + if ( defaultValue > maxValue ) defaultValue = maxValue; + minValue *= scale; + maxValue *= scale; + defaultValue *= scale; + addSlider(label, minValue, maxValue, defaultValue, scale, digits); + } + + /** Author: Michael Kaul */ + private static int digits(double d) { + if (d == (int)d) + return 0; + String s = Double.toString(d); + int ePos = s.indexOf("E"); + if (ePos==-1) + ePos = s.indexOf("e"); + int dotPos = s.indexOf( "." ); + int digits = 0; + if (ePos==-1 ) + digits = s.substring(dotPos+1).length(); + else { + String number = s.substring( dotPos + 1, ePos ); + if (!number.equals( "0" )) + digits += number.length( ); + digits = digits - Integer.valueOf(s.substring(ePos+1)); + } + return digits; + } + + private void addSlider(String label, double minValue, double maxValue, double defaultValue, double scale, int digits) { + int columns = 4 + digits - 2; + if ( columns < 4 ) columns = 4; + if (minValue<0.0) columns++; + String mv = IJ.d2s(maxValue,0); + if (mv.length()>4 && digits==0) + columns += mv.length()-4; + String label2 = label; + if (label2.indexOf('_')!=-1) + label2 = label2.replace('_', ' '); + Label fieldLabel = makeLabel(label2); + this.lastLabelAdded = fieldLabel; + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + c.insets.bottom += 3; + addToSameRow = false; + } else { + c.gridx = 0; c.gridy++; + c.insets = getInsets(0, 0, 3, 0); // top, left, bottom, right + } + c.anchor = GridBagConstraints.EAST; + c.gridwidth = 1; + add(fieldLabel, c); + + if (slider==null) { + slider = new Vector(5); + sliderIndexes = new Vector(5); + sliderScales = new Vector(5); + sliderDigits = new Vector(5); + } + Scrollbar s = new Scrollbar(Scrollbar.HORIZONTAL, (int)defaultValue, 1, (int)minValue, (int)maxValue+1); + GUI.fixScrollbar(s); + slider.addElement(s); + s.addAdjustmentListener(this); + s.setUnitIncrement(1); + if (IJ.isMacOSX()) + s.addKeyListener(this); + + if (numberField==null) { + numberField = new Vector(5); + defaultValues = new Vector(5); + defaultText = new Vector(5); + } + if (IJ.isWindows()) columns -= 2; + if (columns<1) columns = 1; + //IJ.log("scale=" + scale + ", columns=" + columns + ", digits=" + digits); + TextField tf = new TextField(IJ.d2s(defaultValue/scale, digits), columns); + if (IJ.isLinux()) tf.setBackground(Color.white); + tf.addActionListener(this); + tf.addTextListener(this); + tf.addFocusListener(this); + tf.addKeyListener(this); + numberField.addElement(tf); + sliderIndexes.add(new Integer(numberField.size()-1)); + sliderScales.add(new Double(scale)); + sliderDigits.add(new Integer(digits)); + defaultValues.addElement(new Double(defaultValue/scale)); + defaultText.addElement(tf.getText()); + tf.setEditable(true); + firstSlider = false; + + Panel panel = new Panel(); + GridBagLayout pgrid = new GridBagLayout(); + GridBagConstraints pc = new GridBagConstraints(); + panel.setLayout(pgrid); + pc.gridx = 0; pc.gridy = 0; + pc.gridwidth = 1; + pc.ipadx = 85; + pc.anchor = GridBagConstraints.WEST; + panel.add(s, pc); + pc.ipadx = 0; // reset + // text field + pc.gridx = 1; + pc.insets = new Insets(5, 5, 0, 0); + pc.anchor = GridBagConstraints.EAST; + panel.add(tf, pc); + + c.gridx = GridBagConstraints.RELATIVE; + c.gridwidth = 1; + c.anchor = GridBagConstraints.WEST; + c.insets.left = 0; + c.insets.bottom -= 3; + add(panel, c); + if (Recorder.record || macro) + saveLabel(tf, label); + } + + /** Adds a Panel to the dialog. */ + public void addPanel(Panel panel) { + addPanel(panel, GridBagConstraints.WEST, addToSameRow ? c.insets : getInsets(5,0,0,0)); + } + + /** Adds a Panel to the dialog with custom contraint and insets. The + defaults are GridBagConstraints.WEST (left justified) and + "new Insets(5, 0, 0, 0)" (5 pixels of padding at the top). */ + public void addPanel(Panel panel, int constraints, Insets insets) { + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + addToSameRow = false; + } else { + c.gridx = 0; c.gridy++; + } + c.gridwidth = 2; + c.anchor = constraints; + c.insets = insets; + add(panel, c); + } + + /** Adds an image to the dialog. */ + public void addImage(ImagePlus image) { + ImagePanel imagePanel = new ImagePanel(image); + addPanel(imagePanel); + if (imagePanels==null) + imagePanels = new Vector(); + imagePanels.add(imagePanel); + } + + /** Set the insets (margins), in pixels, that will be + used for the next component added to the dialog + (except components added to the same row with addToSameRow) +
+    Default insets:
+        addMessage: 0,20,0 (empty string) or 10,20,0
+        addCheckbox: 15,20,0 (first checkbox) or 0,20,0
+        addCheckboxGroup: 10,0,0
+        addRadioButtonGroup: 5,10,0
+        addNumericField: 5,0,3 (first field) or 0,0,3
+        addStringField: 5,0,5 (first field) or 0,0,5
+        addChoice: 5,0,5 (first field) or 0,0,5
+     
+ */ + public void setInsets(int top, int left, int bottom) { + topInset = top; + leftInset = left; + bottomInset = bottom; + customInsets = true; + } + + /** Makes the next item appear in the same row as the previous. + * May be used for addNumericField, addSlider, addChoice, addCheckbox, addStringField, + * addMessage, addPanel, and before the showDialog() method + * (in the latter case, the buttons appear to the right of the previous item). + * Note that addMessage (and addStringField, if its column width is more than 8) use + * the remaining width, so it must be the last item of a row. + */ + public void addToSameRow() { + addToSameRow = true; + addToSameRowCalled = true; + } + + /** Sets a replacement label for the "OK" button. */ + public void setOKLabel(String label) { + okay.setLabel(label); + } + + /** Sets a replacement label for the "Cancel" button. */ + public void setCancelLabel(String label) { + cancel.setLabel(label); + } + + /** Sets a replacement label for the "Help" button. */ + public void setHelpLabel(String label) { + helpLabel = label; + } + + /** Unchanged parameters are not recorder in 'smart recording' mode. */ + public void setSmartRecording(boolean smartRecording) { + this.smartRecording = smartRecording; + } + + /** Make this a "Yes No Cancel" dialog. */ + public void enableYesNoCancel() { + enableYesNoCancel(" Yes ", " No "); + } + + /** Make this a "Yes No Cancel" dialog with custom labels. Here is an example: +
+        GenericDialog gd = new GenericDialog("YesNoCancel Demo");
+        gd.addMessage("This is a custom YesNoCancel dialog");
+        gd.enableYesNoCancel("Do something", "Do something else");
+        gd.showDialog();
+        if (gd.wasCanceled())
+            IJ.log("User clicked 'Cancel'");
+        else if (gd.wasOKed())
+            IJ. log("User clicked 'Yes'");
+        else
+            IJ. log("User clicked 'No'");
+    	
+ */ + public void enableYesNoCancel(String yesLabel, String noLabel) { + okay.setLabel(yesLabel); + if (no != null) + no.setLabel(noLabel); + else if (noLabel!=null) + no = new Button(noLabel); + } + + /** Do not display "Cancel" button. */ + public void hideCancelButton() { + hideCancelButton = true; + } + + Insets getInsets(int top, int left, int bottom, int right) { + if (customInsets) { + customInsets = false; + return new Insets(topInset, leftInset, bottomInset, 0); + } else + return new Insets(top, left, bottom, right); + } + + /** Add an Object implementing the DialogListener interface. This object will + * be notified by its dialogItemChanged method of input to the dialog. The first + * DialogListener will be also called after the user has typed 'OK' or if the + * dialog has been invoked by a macro; it should read all input fields of the + * dialog. + * For other listeners, the OK button will not cause a call to dialogItemChanged; + * the CANCEL button will never cause such a call. + * @param dl the Object that wants to listen. + */ + public void addDialogListener(DialogListener dl) { + if (dialogListeners == null) + dialogListeners = new Vector(); + dialogListeners.addElement(dl); + } + + /** Returns true if the user clicked on "Cancel". */ + public boolean wasCanceled() { + if (wasCanceled && !Thread.currentThread().getName().endsWith("Script_Macro$")) + Macro.abort(); + return wasCanceled; + } + + /** Returns true if the user has clicked on "OK" or a macro is running. */ + public boolean wasOKed() { + return wasOKed || macro; + } + + /** Returns the contents of the next numeric field, + or NaN if the field does not contain a number. */ + public double getNextNumber() { + if (numberField==null) + return -1.0; + TextField tf = (TextField)numberField.elementAt(nfIndex); + String theText = tf.getText(); + String label=null; + if (macro) { + label = (String)labels.get((Object)tf); + theText = Macro.getValue(macroOptions, label, theText); + } + String originalText = (String)defaultText.elementAt(nfIndex); + double defaultValue = ((Double)(defaultValues.elementAt(nfIndex))).doubleValue(); + double value; + boolean skipRecording = false; + if (theText.equals(originalText)) { + value = defaultValue; + if (smartRecording) skipRecording=true; + } else { + Double d = getValue(theText); + if (d!=null) + value = d.doubleValue(); + else { + // Is the value a macro variable? + if (theText.startsWith("&")) theText = theText.substring(1); + Interpreter interp = Interpreter.getInstance(); + value = interp!=null?interp.getVariable2(theText):Double.NaN; + if (Double.isNaN(value)) { + invalidNumber = true; + errorMessage = "\""+theText+"\" is an invalid number"; + value = Double.NaN; + if (macro) { + IJ.error("Macro Error", "Numeric value expected in run() function\n \n" + +" Dialog box title: \""+getTitle()+"\"\n" + +" Key: \""+label.toLowerCase(Locale.US)+"\"\n" + +" Value or variable name: \""+theText+"\""); + } + } + } + } + if (recorderOn && !skipRecording) { + recordOption(tf, trim(theText)); + } + nfIndex++; + return value; + } + + private String trim(String value) { + if (value.endsWith(".0")) + value = value.substring(0, value.length()-2); + if (value.endsWith(".00")) + value = value.substring(0, value.length()-3); + return value; + } + + private void recordOption(Object component, String value) { + String label = (String)labels.get(component); + if (value.equals("")) value = "[]"; + Recorder.recordOption(label, value); + } + + private void recordCheckboxOption(Checkbox cb) { + String label = (String)labels.get((Object)cb); + if (label!=null) { + if (cb.getState()) // checked + Recorder.recordOption(label); + else if (Recorder.getCommandOptions()==null) + Recorder.recordOption(" "); + } + } + + protected Double getValue(String text) { + Double d; + try {d = new Double(text);} + catch (NumberFormatException e){ + d = null; + } + return d; + } + + public double parseDouble(String s) { + if (s==null) return Double.NaN; + double value = Tools.parseDouble(s); + if (Double.isNaN(value)) { + if (s.startsWith("&")) s = s.substring(1); + Interpreter interp = Interpreter.getInstance(); + value = interp!=null?interp.getVariable2(s):Double.NaN; + } + return value; + } + + /** Returns true if one or more of the numeric fields contained an + invalid number. Must be called after one or more calls to getNextNumber(). */ + public boolean invalidNumber() { + boolean wasInvalid = invalidNumber; + invalidNumber = false; + return wasInvalid; + } + + /** Returns an error message if getNextNumber was unable to convert a + string into a number, otherwise, returns null. */ + public String getErrorMessage() { + return errorMessage; + } + + /** Returns the contents of the next text field. */ + public String getNextString() { + String theText; + if (stringField==null) + return ""; + TextField tf = (TextField)(stringField.elementAt(sfIndex)); + theText = tf.getText(); + String label = labels!=null?(String)labels.get((Object)tf):""; + if (macro) { + theText = Macro.getValue(macroOptions, label, theText); + if (theText!=null && (theText.startsWith("&")||label.toLowerCase(Locale.US).startsWith(theText))) { + // Is the value a macro variable? + if (theText.startsWith("&")) theText = theText.substring(1); + Interpreter interp = Interpreter.getInstance(); + String s = interp!=null?interp.getVariableAsString(theText):null; + if (s!=null) theText = s; + } + } + if (recorderOn && !label.equals("")) { + String s = theText; + if (s!=null&&s.length()>=3&&Character.isLetter(s.charAt(0))&&s.charAt(1)==':'&&s.charAt(2)=='\\') + s = s.replaceAll("\\\\", "/"); // replace "\" with "/" in Windows file paths + s = Recorder.fixString(s); + if (!smartRecording || !s.equals((String)defaultStrings.elementAt(sfIndex))) + recordOption(tf, s); + else if (Recorder.getCommandOptions()==null) + Recorder.recordOption(" "); + } + sfIndex++; + return theText; + } + + /** Returns the state of the next checkbox. */ + public boolean getNextBoolean() { + if (checkbox==null) + return false; + Checkbox cb = (Checkbox)(checkbox.elementAt(cbIndex)); + if (recorderOn) + recordCheckboxOption(cb); + boolean state = cb.getState(); + if (macro) { + String label = (String)labels.get((Object)cb); + String key = Macro.trimKey(label); + state = isMatch(macroOptions, key+" "); + } + cbIndex++; + return state; + } + + // Returns true if s2 is in s1 and not in a bracketed literal (e.g., "[literal]") + boolean isMatch(String s1, String s2) { + if (s1.startsWith(s2)) + return true; + s2 = " " + s2; + int len1 = s1.length(); + int len2 = s2.length(); + boolean match, inLiteral=false; + char c; + for (int i=0; i1&&s1.charAt(i-1)=='=')) + continue; + match = true; + for (int j=0; j0) { + resetCounters(); + ((DialogListener)dialogListeners.elementAt(0)).dialogItemChanged(this,null); + recorderOn = false; + } + resetCounters(); + } + + @Override + public void setFont(Font font) { + super.setFont(!fontSizeSet&&Prefs.getGuiScale()!=1.0&&font!=null?font.deriveFont((float)(font.getSize()*Prefs.getGuiScale())):font); + fontSizeSet = true; + } + + /** Reset the counters before reading the dialog parameters */ + void resetCounters() { + nfIndex = 0; // prepare for readout + sfIndex = 0; + cbIndex = 0; + choiceIndex = 0; + textAreaIndex = 0; + radioButtonIndex = 0; + invalidNumber = false; + } + + /** Returns the Vector containing the numeric TextFields. */ + public Vector getNumericFields() { + return numberField; + } + + /** Returns the Vector containing the string TextFields. */ + public Vector getStringFields() { + return stringField; + } + + /** Returns the Vector containing the Checkboxes. */ + public Vector getCheckboxes() { + return checkbox; + } + + /** Returns the Vector containing the Choices. */ + public Vector getChoices() { + return choice; + } + + /** Returns the Vector containing the sliders (Scrollbars). */ + public Vector getSliders() { + return slider; + } + + /** Returns the Vector that contains the RadioButtonGroups. */ + public Vector getRadioButtonGroups() { + return radioButtonGroups; + } + + /** Returns a reference to textArea1. */ + public TextArea getTextArea1() { + return textArea1; + } + + /** Returns a reference to textArea2. */ + public TextArea getTextArea2() { + return textArea2; + } + + /** Returns a reference to the Label or MultiLineLabel created by the + * last addMessage() call. Otherwise returns null. */ + public Component getMessage() { + return theLabel; + } + + /** Returns a reference to the Preview checkbox. */ + public Checkbox getPreviewCheckbox() { + return previewCheckbox; + } + + /** Returns 'true' if this dialog has a "Preview" checkbox and it is enabled. */ + public boolean isPreviewActive() { + return previewCheckbox!=null && previewCheckbox.getState(); + } + + /** Returns references to the "OK" ("Yes"), "Cancel", + and if present, "No" buttons as an array. */ + public Button[] getButtons() { + Button[] buttons = new Button[3]; + buttons[0] = okay; + buttons[1] = cancel; + buttons[2] = no; + return buttons; + } + + /** Used by PlugInFilterRunner to provide visable feedback whether preview + is running or not by switching from "Preview" to "wait..." + */ + public void previewRunning(boolean isRunning) { + if (previewCheckbox!=null) { + previewCheckbox.setLabel(isRunning ? previewRunning : previewLabel); + if (IJ.isMacOSX()) repaint(); //workaround OSX 10.4 refresh bug + } + } + + /** Display dialog centered on the primary screen. */ + public void centerDialog(boolean b) { + centerDialog = b; + } + + /* Display the dialog at the specified location. */ + public void setLocation(int x, int y) { + super.setLocation(x, y); + centerDialog = false; + } + + public void setDefaultString(int index, String str) { + if (defaultStrings!=null && index>=0 && index". There is an example at + http://imagej.nih.gov/ij/macros/js/DialogWithHelp.js + */ + public void addHelp(String url) { + helpURL = url; + } + + void showHelp() { + if (helpURL.startsWith("")) { + String title = getTitle()+" "+helpLabel; + if (this instanceof NonBlockingGenericDialog) + new HTMLDialog(title, helpURL, false); // non blocking + else + new HTMLDialog(this, title, helpURL); //modal + } else { + String macro = "call('ij.plugin.BrowserLauncher.open', '"+helpURL+"');"; + new MacroRunner(macro); // open on separate thread using BrowserLauncher + } + } + + protected boolean isMacro() { + return macro; + } + + public static GenericDialog getInstance() { + return instance; + } + + /** Closes the dialog; records the options */ + public void dispose() { + super.dispose(); + instance = null; + + if (!macro) { + recorderOn = Recorder.record; + IJ.wait(25); + } + resetCounters(); + finalizeRecording(); + resetCounters(); + } + + /** Returns a reference to the label of the most recently + added numeric field, string field, choice or slider. */ + public Label getLabel() { + return lastLabelAdded; + } + + public void windowActivated(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + + @SuppressWarnings("unchecked") + static String getString(DropTargetDropEvent event) + throws IOException, UnsupportedFlavorException { + String text = null; + DataFlavor fileList = DataFlavor.javaFileListFlavor; + + if (event.isDataFlavorSupported(fileList)) { + event.acceptDrop(DnDConstants.ACTION_COPY); + java.util.List list = (java.util.List)event.getTransferable().getTransferData(fileList); + text = list.get(0).getAbsolutePath(); + } + else if (event.isDataFlavorSupported(DataFlavor.stringFlavor)) { + event.acceptDrop(DnDConstants.ACTION_COPY); + text = (String)event.getTransferable() + .getTransferData(DataFlavor.stringFlavor); + if (text.startsWith("file://")) + text = text.substring(7); + text = stripSuffix(stripSuffix(text, "\n"), + "\r").replaceAll("%20", " "); + } + else { + event.rejectDrop(); + return null; + } + + event.dropComplete(text != null); + return text; + } + + static String stripSuffix(String s, String suffix) { + return !s.endsWith(suffix) ? s : + s.substring(0, s.length() - suffix.length()); + } + + static class TextDropTarget extends DropTargetAdapter { + TextField text; + DataFlavor flavor = DataFlavor.stringFlavor; + + public TextDropTarget(TextField text) { + this.text = text; + } + + @Override + public void drop(DropTargetDropEvent event) { + try { + text.setText(getString(event)); + } catch (Exception e) { e.printStackTrace(); } + } + } + + private class BrowseButtonListener implements ActionListener { + private String label; + private TextField textField; + private String mode; + + public BrowseButtonListener(String label, TextField textField, String mode) { + this.label = label; + this.textField = textField; + this.mode = mode; + } + + public void actionPerformed(ActionEvent e) { + String path = null; + if (mode.equals("dir")) { + path = IJ.getDir("Select a Folder"); + } else { + OpenDialog od = new OpenDialog("Select a File", null); + String directory = od.getDirectory(); + String name = od.getFileName(); + if (name!=null) + path = directory+name; + } + if (path!=null) + this.textField.setText(path); + } + + } + +} diff --git a/src/ij/gui/HTMLDialog.java b/src/ij/gui/HTMLDialog.java new file mode 100644 index 0000000..a08fe4d --- /dev/null +++ b/src/ij/gui/HTMLDialog.java @@ -0,0 +1,149 @@ +package ij.gui; +import ij.*; +import ij.plugin.URLOpener; +import ij.macro.MacroRunner; +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.text.*; +import javax.swing.text.html.*; +import javax.swing.event.HyperlinkListener; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkEvent.EventType; +import java.net.URL; + +/** This is modal or non-modal dialog box that displays HTML formated text. */ +public class HTMLDialog extends JDialog implements ActionListener, KeyListener, HyperlinkListener, WindowListener { + private boolean escapePressed; + private JEditorPane editorPane; + private boolean modal = true; + + public HTMLDialog(String title, String message) { + super(ij.IJ.getInstance(), title, true); + init(message); + } + + public HTMLDialog(Dialog parent, String title, String message) { + super(parent, title, true); + init(message); + } + + public HTMLDialog(String title, String message, boolean modal) { + super(ij.IJ.getInstance(), title, modal); + this.modal = modal; + init(message); + } + + private void init(String message) { + ij.util.Java2.setSystemLookAndFeel(); + Container container = getContentPane(); + container.setLayout(new BorderLayout()); + if (message==null) message = ""; + editorPane = new JEditorPane("text/html",""); + editorPane.setEditable(false); + HTMLEditorKit kit = new HTMLEditorKit(); + editorPane.setEditorKit(kit); + StyleSheet styleSheet = kit.getStyleSheet(); + styleSheet.addRule("body{font-family:Verdana,sans-serif; font-size:11.5pt; margin:5px 10px 5px 10px;}"); //top right bottom left + styleSheet.addRule("h1{font-size:18pt;}"); + styleSheet.addRule("h2{font-size:15pt;}"); + styleSheet.addRule("dl dt{font-face:bold;}"); + editorPane.setText(message); //display the html text with the above style + editorPane.getActionMap().put("insert-break", new AbstractAction(){ + public void actionPerformed(ActionEvent e) {} + }); //suppress beep on key + JScrollPane scrollPane = new JScrollPane(editorPane); + container.add(scrollPane); + JButton button = new JButton("OK"); + button.addActionListener(this); + button.addKeyListener(this); + editorPane.addKeyListener(this); + editorPane.addHyperlinkListener(this); + JPanel panel = new JPanel(); + panel.add(button); + container.add(panel, "South"); + setForeground(Color.black); + addWindowListener(this); + pack(); + Dimension screenD = IJ.getScreenSize(); + Dimension dialogD = getSize(); + int maxWidth = (int)(Math.min(0.70*screenD.width, 800)); //max 70% of screen width, but not more than 800 pxl + if (maxWidth>400 && dialogD.width>maxWidth) + dialogD.width = maxWidth; + if (dialogD.height > 0.80*screenD.height && screenD.height>400) //max 80% of screen height + dialogD.height = (int)(0.80*screenD.height); + setSize(dialogD); + GUI.centerOnImageJScreen(this); + if (!modal) { + WindowManager.addWindow(this); + show(); + } + final JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar(); + if (verticalScrollBar!=null) { + EventQueue.invokeLater(new Runnable() { + public void run() { + verticalScrollBar.setValue(verticalScrollBar.getMinimum()); //start scrollbar at top + } + }); + } + if (modal) show(); + } + + public void actionPerformed(ActionEvent e) { + dispose(); + } + + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + ij.IJ.setKeyDown(keyCode); + escapePressed = keyCode==KeyEvent.VK_ESCAPE; + if (keyCode==KeyEvent.VK_C) { + if (editorPane.getSelectedText()==null || editorPane.getSelectedText().length()==0) + editorPane.selectAll(); + editorPane.copy(); + editorPane.select(0,0); + } else if (keyCode==KeyEvent.VK_ENTER || keyCode==KeyEvent.VK_W || escapePressed) + dispose(); + } + + public void keyReleased(KeyEvent e) { + int keyCode = e.getKeyCode(); + ij.IJ.setKeyUp(keyCode); + } + + public void keyTyped(KeyEvent e) {} + + public boolean escapePressed() { + return escapePressed; + } + + public void hyperlinkUpdate(HyperlinkEvent e) { + if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + String url = e.getDescription(); //getURL does not work for relative links within document such as "#top" + if (url==null) return; + if (url.startsWith("#")) + editorPane.scrollToReference(url.substring(1)); + else { + String macro = "run('URL...', 'url="+url+"');"; + new MacroRunner(macro); + } + } + } + + public void dispose() { + super.dispose(); + if (!modal) WindowManager.removeWindow(this); + } + + public void windowClosing(WindowEvent e) { + dispose(); + } + + public void windowActivated(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + +} diff --git a/src/ij/gui/HistogramPlot.java b/src/ij/gui/HistogramPlot.java new file mode 100644 index 0000000..9e346fc --- /dev/null +++ b/src/ij/gui/HistogramPlot.java @@ -0,0 +1,382 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.plugin.filter.Analyzer; +import ij.measure.*; +import ij.macro.Interpreter; +import java.awt.*; +import java.awt.image.*; + +public class HistogramPlot extends ImagePlus { + static final double SCALE = Prefs.getGuiScale(); + static final int HIST_WIDTH = (int)(SCALE*256); + static final int HIST_HEIGHT = (int)(SCALE*128); + static final int XMARGIN = (int)(20*SCALE); + static final int YMARGIN = (int)(10*SCALE); + static final int WIN_WIDTH = HIST_WIDTH + (int)(44*SCALE); + static final int WIN_HEIGHT = HIST_HEIGHT + (int)(118*SCALE); + static final int BAR_HEIGHT = (int)(SCALE*12); + static final int INTENSITY1=0, INTENSITY2=1, RGB=2, RED=3, GREEN=4, BLUE=5; + static final Color frameColor = new Color(30,60,120); + + int rgbMode = -1; + ImageStatistics stats; + boolean stackHistogram; + Calibration cal; + long[] histogram; + LookUpTable lut; + int decimalPlaces; + int digits; + long newMaxCount; + boolean logScale; + int yMax; + int srcImageID; + Rectangle frame; + Font font = new Font("SansSerif",Font.PLAIN,(int)(12*SCALE)); + boolean showBins; + int col1, col2, row1, row2, row3, row4, row5; + + public HistogramPlot() { + setImage(NewImage.createRGBImage("Histogram", WIN_WIDTH, WIN_HEIGHT, 1, NewImage.FILL_WHITE)); + } + + /** Plots a histogram using the specified title and number of bins. + Currently, the number of bins must be 256 expect for 32 bit images. */ + public void draw(String title, ImagePlus imp, int bins) { + draw(imp, bins, 0.0, 0.0, 0); + } + + /** Plots a histogram using the specified title, number of bins and histogram range. + Currently, the number of bins must be 256 and the histogram range range must be + the same as the image range expect for 32 bit images. */ + public void draw(ImagePlus imp, int bins, double histMin, double histMax, int yMax) { + boolean limitToThreshold = (Analyzer.getMeasurements()&LIMIT)!=0; + ImageProcessor ip = imp.getProcessor(); + if (ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD + && ip.getLutUpdateMode()==ImageProcessor.NO_LUT_UPDATE) + limitToThreshold = false; // ignore invisible thresholds + if (imp.isRGB() && rgbMode maxCount2) && (i != stats.mode)) { + maxCount2 = histogram[i]; + mode2 = i; + } + } + newMaxCount = histogram[stats.mode]; + if ((newMaxCount>(maxCount2 * 2)) && (maxCount2 != 0)) + newMaxCount = (int)(maxCount2 * 1.5); + if (logScale) + drawLogPlot(yMax>0?yMax:newMaxCount, ip); + drawPlot(yMax>0?yMax:newMaxCount, ip); + histogram[stats.mode] = saveModalCount; + x = XMARGIN + 1; + y = YMARGIN + HIST_HEIGHT + 2; + if (imp==null) + lut.drawUnscaledColorBar(ip, x-1, y, HIST_WIDTH, BAR_HEIGHT); + else + drawAlignedColorBar(imp, xMin, xMax, ip, x-1, y, HIST_WIDTH, BAR_HEIGHT); + y += BAR_HEIGHT+(int)(15*SCALE); + drawText(ip, x, y, fixedRange); + srcImageID = imp.getID(); + } + + void drawAlignedColorBar(ImagePlus imp, double xMin, double xMax, ImageProcessor ip, int x, int y, int width, int height) { + ImageProcessor ipSource = imp.getProcessor(); + float[] pixels = null; + ImageProcessor ipRamp = null; + if (rgbMode>=INTENSITY1) { + ipRamp = new FloatProcessor(width, height); + if (rgbMode==RED) + ipRamp.setColorModel(LUT.createLutFromColor(Color.red)); + else if (rgbMode==GREEN) + ipRamp.setColorModel(LUT.createLutFromColor(Color.green)); + else if (rgbMode==BLUE) + ipRamp.setColorModel(LUT.createLutFromColor(Color.blue)); + pixels = (float[])ipRamp.getPixels(); + } else + pixels = new float[width*height]; + for (int j=0; jipSource.getPixelCount()) { // stack histogram + cm = LUT.createLutFromColor(Color.white); + min = stats.min; + max = stats.max; + } else + cm = ((CompositeImage)imp).getChannelLut(); + } else if (ipSource.getMinThreshold()==ImageProcessor.NO_THRESHOLD) + cm = ipSource.getColorModel(); + else + cm = ipSource.getCurrentColorModel(); + ipRamp = new FloatProcessor(width, height, pixels, cm); + } + ipRamp.setMinAndMax(min,max); + ImageProcessor bar = null; + if (ip instanceof ColorProcessor) + bar = ipRamp.convertToRGB(); + else + bar = ipRamp.convertToByte(true); + ip.insert(bar, x,y); + ip.setColor(Color.black); + ip.drawRect(x-1, y, width+2, height); + } + + /** Scales a threshold level to the range 0-255. */ + int scaleDown(ImageProcessor ip, double threshold) { + double min = ip.getMin(); + double max = ip.getMax(); + if (max>min) + return (int)(((threshold-min)/(max-min))*255.0); + else + return 0; + } + + void drawPlot(long maxCount, ImageProcessor ip) { + if (maxCount==0) maxCount = 1; + frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT); + if (histogram.length==256) { + double scale2 = HIST_WIDTH/256.0; + int barWidth = 1; + if (SCALE>1) barWidth=2; + if (SCALE>2) barWidth=3; + for (int i = 0; i < 256; i++) { + int x =(int)(i*scale2); + int y = (int)(((double)HIST_HEIGHT*(double)histogram[i])/maxCount); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + for (int j = 0; jHIST_HEIGHT) y = HIST_HEIGHT; + ip.drawLine(i+XMARGIN, YMARGIN+HIST_HEIGHT, i+XMARGIN, YMARGIN+HIST_HEIGHT-y); + } + } else { + double xscale = (double)HIST_WIDTH/histogram.length; + for (int i=0; i0L) { + int y = (int)(((double)HIST_HEIGHT*(double)value)/maxCount); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + int x = (int)(i*xscale)+XMARGIN; + ip.drawLine(x, YMARGIN+HIST_HEIGHT, x, YMARGIN+HIST_HEIGHT-y); + } + } + } + ip.setColor(frameColor); + ip.drawRect(frame.x-1, frame.y, frame.width+2, frame.height+1); + ip.setColor(Color.black); + } + + void drawLogPlot (long maxCount, ImageProcessor ip) { + frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT); + ip.drawRect(frame.x-1, frame.y, frame.width+2, frame.height+1); + double max = Math.log(maxCount); + ip.setColor(Color.gray); + if (histogram.length==256) { + double scale2 = HIST_WIDTH/256.0; + int barWidth = 1; + if (SCALE>1) barWidth=2; + if (SCALE>2) barWidth=3; + for (int i=0; i < 256; i++) { + int x =(int)(i*scale2); + int y = histogram[i]==0?0:(int)(HIST_HEIGHT*Math.log(histogram[i])/max); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + for (int j = 0; jHIST_HEIGHT) y = HIST_HEIGHT; + ip.drawLine(i+XMARGIN, YMARGIN+HIST_HEIGHT, i+XMARGIN, YMARGIN+HIST_HEIGHT-y); + } + } else { + double xscale = (double)HIST_WIDTH/histogram.length; + for (int i=0; i0L) { + int y = (int)(HIST_HEIGHT*Math.log(value)/max); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + int x = (int)(i*xscale)+XMARGIN; + ip.drawLine(x, YMARGIN+HIST_HEIGHT, x, YMARGIN+HIST_HEIGHT-y); + } + } + } + ip.setColor(Color.black); + } + + void drawText(ImageProcessor ip, int x, int y, boolean fixedRange) { + ip.setFont(font); + ip.setAntialiasedText(true); + double hmin = cal.getCValue(stats.histMin); + double hmax = cal.getCValue(stats.histMax); + double range = hmax-hmin; + if (fixedRange&&!cal.calibrated()&&hmin==0&&hmax==255) + range = 256; + ip.drawString(d2s(hmin), x - 4, y); + ip.drawString(d2s(hmax), x + HIST_WIDTH - getWidth(hmax, ip) + 10, y); + if (rgbMode>=INTENSITY1) { + x += HIST_WIDTH/2; + y += 1; + ip.setJustification(ImageProcessor.CENTER_JUSTIFY); + boolean weighted = ((ColorProcessor)ip).weightedHistogram(); + switch (rgbMode) { + case INTENSITY1: ip.drawString((weighted?"Intensity (weighted)":"Intensity (unweighted)"), x, y); break; + case INTENSITY2: ip.drawString((weighted?"Intensity (unweighted)":"Intensity (weighted)"), x, y); break; + case RGB: ip.drawString("R+G+B", x, y); break; + case RED: ip.drawString("Red", x, y); break; + case GREEN: ip.drawString("Green", x, y); break; + case BLUE: ip.drawString("Blue", x, y); break; + } + ip.setJustification(ImageProcessor.LEFT_JUSTIFY); + } + double binWidth = range/stats.nBins; + binWidth = Math.abs(binWidth); + showBins = binWidth!=1.0 || !fixedRange; + col1 = XMARGIN + 5; + col2 = XMARGIN + HIST_WIDTH/2; + row1 = y+(int)(25*SCALE); + if (showBins) row1 -= (int)(8*SCALE); + row2 = row1 + (int)(15*SCALE); + row3 = row2 + (int)(15*SCALE); + row4 = row3 + (int)(15*SCALE); + row5 = row4 + (int)(15*SCALE); + long count = stats.longPixelCount>0?stats.longPixelCount:stats.pixelCount; + String modeCount = " (" + stats.maxCount + ")"; + if (modeCount.length()>12) modeCount = ""; + + ip.drawString("N: " + count, col1, row1); + ip.drawString("Min: " + d2s(stats.min), col2, row1); + ip.drawString("Mean: " + d2s(stats.mean), col1, row2); + ip.drawString("Max: " + d2s(stats.max), col2, row2); + ip.drawString("StdDev: " + d2s(stats.stdDev), col1, row3); + ip.drawString("Mode: " + d2s(stats.dmode) + modeCount, col2, row3); + if (showBins) { + ip.drawString("Bins: " + d2s(stats.nBins), col1, row4); + ip.drawString("Bin Width: " + d2s(binWidth), col2, row4); + } + } + + private String d2s(double d) { + if ((int)d==d) + return IJ.d2s(d, 0); + else + return IJ.d2s(d, 3, 8); + } + + int getWidth(double d, ImageProcessor ip) { + return ip.getStringWidth(d2s(d)); + } + + public int[] getHistogram() { + int[] hist = new int[histogram.length]; + for (int i=0; i=frame.x && x<=(frame.x+frame.width)) { + x = (x - frame.x); + int index = (int)(x*(SCALE*histogram.length)/HIST_WIDTH/SCALE); + if (index>=histogram.length) index = histogram.length-1; + double value = cal.getCValue(stats.histMin+index*stats.binSize); + drawValueAndCount(ip, value, histogram[index]); + } else + drawValueAndCount(ip, Double.NaN, -1); + this.imp.updateAndDraw(); + } + + protected void drawHistogram(ImageProcessor ip, boolean fixedRange) { + drawHistogram(null, ip, fixedRange, 0.0, 0.0); + } + + void drawHistogram(ImagePlus imp, ImageProcessor ip, boolean fixedRange, double xMin, double xMax) { + int x, y; + long maxCount2 = 0; + int mode2 = 0; + long saveModalCount; + ip.setColor(Color.black); + ip.setLineWidth(1); + decimalPlaces = Analyzer.getPrecision(); + digits = cal.calibrated()||stats.binSize!=1.0?decimalPlaces:0; + saveModalCount = histogram[stats.mode]; + for (int i = 0; i maxCount2) && (i != stats.mode)) { + maxCount2 = histogram[i]; + mode2 = i; + } + } + newMaxCount = histogram[stats.mode]; + if ((newMaxCount>(maxCount2 * 2)) && (maxCount2 != 0)) + newMaxCount = (int)(maxCount2 * 1.5); + if (logScale || IJ.shiftKeyDown() && !liveMode()) + drawLogPlot(yMax>0?yMax:newMaxCount, ip); + drawPlot(yMax>0?yMax:newMaxCount, ip); + histogram[stats.mode] = saveModalCount; + x = XMARGIN + 1; + y = YMARGIN + HIST_HEIGHT + 2; + if (imp==null) + lut.drawUnscaledColorBar(ip, x-1, y, HIST_WIDTH, BAR_HEIGHT); + else + drawAlignedColorBar(imp, xMin, xMax, ip, x-1, y, HIST_WIDTH, BAR_HEIGHT); + y += BAR_HEIGHT+(int)(15*SCALE); + drawText(ip, x, y, fixedRange); + srcImageID = imp.getID(); + } + + void drawAlignedColorBar(ImagePlus imp, double xMin, double xMax, ImageProcessor ip, int x, int y, int width, int height) { + ImageProcessor ipSource = imp.getProcessor(); + float[] pixels = null; + ImageProcessor ipRamp = null; + if (rgbMode>=INTENSITY1) { + ipRamp = new FloatProcessor(width, height); + if (rgbMode==RED) + ipRamp.setColorModel(LUT.createLutFromColor(Color.red)); + else if (rgbMode==GREEN) + ipRamp.setColorModel(LUT.createLutFromColor(Color.green)); + else if (rgbMode==BLUE) + ipRamp.setColorModel(LUT.createLutFromColor(Color.blue)); + pixels = (float[])ipRamp.getPixels(); + } else + pixels = new float[width*height]; + for (int j=0; jipSource.getPixelCount()) { // stack histogram + cm = LUT.createLutFromColor(Color.white); + min = stats.min; + max = stats.max; + } else + cm = ((CompositeImage)imp).getChannelLut(); + } else if (ipSource.getMinThreshold()==ImageProcessor.NO_THRESHOLD) + cm = ipSource.getColorModel(); + else + cm = ipSource.getCurrentColorModel(); + ipRamp = new FloatProcessor(width, height, pixels, cm); + } + ipRamp.setMinAndMax(min,max); + ImageProcessor bar = null; + if (ip instanceof ColorProcessor) + bar = ipRamp.convertToRGB(); + else + bar = ipRamp.convertToByte(true); + ip.insert(bar, x,y); + ip.setColor(Color.black); + ip.drawRect(x-1, y, width+2, height); + } + + /** Scales a threshold level to the range 0-255. */ + int scaleDown(ImageProcessor ip, double threshold) { + double min = ip.getMin(); + double max = ip.getMax(); + if (max>min) + return (int)(((threshold-min)/(max-min))*255.0); + else + return 0; + } + + void drawPlot(long maxCount, ImageProcessor ip) { + if (maxCount==0) maxCount = 1; + frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT); + ip.drawRect(frame.x-1, frame.y, frame.width+2, frame.height+1); + if (histogram.length==256) { + double scale2 = HIST_WIDTH/256.0; + int barWidth = 1; + if (SCALE>1) barWidth=2; + if (SCALE>2) barWidth=3; + for (int i = 0; i < 256; i++) { + int x =(int)(i*scale2); + int y = (int)(((double)HIST_HEIGHT*(double)histogram[i])/maxCount); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + for (int j = 0; jHIST_HEIGHT) y = HIST_HEIGHT; + ip.drawLine(i+XMARGIN, YMARGIN+HIST_HEIGHT, i+XMARGIN, YMARGIN+HIST_HEIGHT-y); + } + } else { + double xscale = (double)HIST_WIDTH/histogram.length; + for (int i=0; i0L) { + int y = (int)(((double)HIST_HEIGHT*(double)value)/maxCount); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + int x = (int)(i*xscale)+XMARGIN; + ip.drawLine(x, YMARGIN+HIST_HEIGHT, x, YMARGIN+HIST_HEIGHT-y); + } + } + } + } + + void drawLogPlot (long maxCount, ImageProcessor ip) { + frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT); + ip.drawRect(frame.x-1, frame.y, frame.width+2, frame.height+1); + double max = Math.log(maxCount); + ip.setColor(Color.gray); + if (histogram.length==256) { + double scale2 = HIST_WIDTH/256.0; + int barWidth = 1; + if (SCALE>1) barWidth=2; + if (SCALE>2) barWidth=3; + for (int i=0; i < 256; i++) { + int x =(int)(i*scale2); + int y = histogram[i]==0?0:(int)(HIST_HEIGHT*Math.log(histogram[i])/max); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + for (int j = 0; jHIST_HEIGHT) y = HIST_HEIGHT; + ip.drawLine(i+XMARGIN, YMARGIN+HIST_HEIGHT, i+XMARGIN, YMARGIN+HIST_HEIGHT-y); + } + } else { + double xscale = (double)HIST_WIDTH/histogram.length; + for (int i=0; i0L) { + int y = (int)(HIST_HEIGHT*Math.log(value)/max); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + int x = (int)(i*xscale)+XMARGIN; + ip.drawLine(x, YMARGIN+HIST_HEIGHT, x, YMARGIN+HIST_HEIGHT-y); + } + } + } + ip.setColor(Color.black); + } + + void drawText(ImageProcessor ip, int x, int y, boolean fixedRange) { + ip.setFont(font); + ip.setAntialiasedText(true); + double hmin = cal.getCValue(stats.histMin); + double hmax = cal.getCValue(stats.histMax); + double range = hmax-hmin; + if (fixedRange&&!cal.calibrated()&&hmin==0&&hmax==255) + range = 256; + ip.drawString(d2s(hmin), x - 4, y); + ip.drawString(d2s(hmax), x + HIST_WIDTH - getWidth(hmax, ip) + 10, y); + if (rgbMode>=INTENSITY1) { + x += HIST_WIDTH/2; + y += 1; + ip.setJustification(ImageProcessor.CENTER_JUSTIFY); + boolean weighted = ((ColorProcessor)ip).weightedHistogram(); + switch (rgbMode) { + case INTENSITY1: ip.drawString((weighted?"Intensity (weighted)":"Intensity (unweighted)"), x, y); break; + case INTENSITY2: ip.drawString((weighted?"Intensity (unweighted)":"Intensity (weighted)"), x, y); break; + case RGB: ip.drawString("R+G+B", x, y); break; + case RED: ip.drawString("Red", x, y); break; + case GREEN: ip.drawString("Green", x, y); break; + case BLUE: ip.drawString("Blue", x, y); break; + } + ip.setJustification(ImageProcessor.LEFT_JUSTIFY); + } + double binWidth = range/stats.nBins; + binWidth = Math.abs(binWidth); + showBins = binWidth!=1.0 || !fixedRange; + col1 = XMARGIN + 5; + col2 = XMARGIN + HIST_WIDTH/2; + row1 = y+(int)(25*SCALE); + if (showBins) row1 -= (int)(8*SCALE); + row2 = row1 + (int)(15*SCALE); + row3 = row2 + (int)(15*SCALE); + row4 = row3 + (int)(15*SCALE); + row5 = row4 + (int)(15*SCALE); + long count = stats.longPixelCount>0?stats.longPixelCount:stats.pixelCount; + String modeCount = " (" + stats.maxCount + ")"; + if (modeCount.length()>12) modeCount = ""; + + ip.drawString("N: " + count, col1, row1); + ip.drawString("Min: " + d2s(stats.min), col2, row1); + ip.drawString("Mean: " + d2s(stats.mean), col1, row2); + ip.drawString("Max: " + d2s(stats.max), col2, row2); + ip.drawString("StdDev: " + d2s(stats.stdDev), col1, row3); + ip.drawString("Mode: " + d2s(stats.dmode) + modeCount, col2, row3); + if (showBins) { + ip.drawString("Bins: " + d2s(stats.nBins), col1, row4); + ip.drawString("Bin Width: " + d2s(binWidth), col2, row4); + } + drawValueAndCount(ip, Double.NaN, -1); + } + + private void drawValueAndCount(ImageProcessor ip, double value, long count) { + int y = showBins?row4:row3; + ip.setRoi(0, y, WIN_WIDTH, WIN_HEIGHT-y); + ip.setColor(Color.white); + ip.fill(); + ip.setColor(Color.black); + String sValue = Double.isNaN(value)?"---":d2s(value); + String sCount = count==-1?"---":""+count; + int row = showBins?row5:row4; + ip.drawString("Value: " + sValue, col1, row); + ip.drawString("Count: " + sCount, col2, row); + } + + private String d2s(double d) { + if ((int)d==d) + return IJ.d2s(d, 0); + else + return IJ.d2s(d, 3, 8); + } + + int getWidth(double d, ImageProcessor ip) { + return ip.getStringWidth(d2s(d)); + } + + /** Returns the histogram values as a ResultsTable. */ + public ResultsTable getResultsTable() { + ResultsTable rt = new ResultsTable(); + rt.setPrecision(digits); + String vheading = stats.binSize==1.0?"value":"bin start"; + if (cal.calibrated() && !cal.isSigned16Bit()) { + for (int i=0; i0?yMax:newMaxCount, ip); + drawPlot(yMax>0?yMax:newMaxCount, ip); + } else + drawPlot(yMax>0?yMax:newMaxCount, ip); + this.imp.updateAndDraw(); + } + + public void actionPerformed(ActionEvent e) { + Object b = e.getSource(); + if (b==live) + toggleLiveMode(); + else if (b==rgb) + changeChannel(); + else if (b==list) + showList(); + else if (b==copy) + copyToClipboard(); + else if (b==log) { + logScale = !logScale; + replot(); + } + } + + public void lostOwnership(Clipboard clipboard, Transferable contents) {} + + public int[] getHistogram() { + int[] hist = new int[histogram.length]; + for (int i=0; iBLUE) rgbMode=INTENSITY1; + ColorProcessor cp = (ColorProcessor)imp.getProcessor(); + boolean weighted = cp.weightedHistogram(); + if (rgbMode==INTENSITY2) { + double[] weights = cp.getRGBWeights(); + if (weighted) + cp.setRGBWeights(1d/3d, 1d/3d, 1d/3d); + else + cp.setRGBWeights(0.299, 0.587, 0.114); + showHistogram(imp, 256); + cp.setRGBWeights(weights); + } else + showHistogram(imp, 256); + } + } + + private boolean liveMode() { + return live!=null && live.getForeground()==Color.red; + } + + private void enableLiveMode() { + if (bgThread==null) { + srcImp = WindowManager.getImage(srcImageID); + if (srcImp==null) return; + bgThread = new Thread(this, "Live Histogram"); + bgThread.setPriority(Math.max(bgThread.getPriority()-3, Thread.MIN_PRIORITY)); + bgThread.start(); + imageUpdated(srcImp); + } + createListeners(); + if (srcImp!=null) + imageUpdated(srcImp); + } + + // Unused + public void imageOpened(ImagePlus imp) { + } + + // This listener is called if the source image content is changed + public synchronized void imageUpdated(ImagePlus imp) { + if (imp==srcImp) { + doUpdate = true; + notify(); + } + } + + public synchronized void roiModified(ImagePlus img, int id) { + if (img==srcImp) { + doUpdate=true; + notify(); + } + } + + // If either the source image or this image are closed, exit + public void imageClosed(ImagePlus imp) { + if (imp==srcImp || imp==this.imp) { + if (bgThread!=null) + bgThread.interrupt(); + bgThread = null; + removeListeners(); + srcImp = null; + } + } + + // the background thread for live plotting. + public void run() { + while (true) { + if (doUpdate && srcImp!=null) { + if (srcImp.getRoi()!=null) + IJ.wait(50); //delay to make sure the roi has been updated + if (srcImp!=null) { + if (srcImp.getBitDepth()==16 && ImagePlus.getDefault16bitRange()!=0) + showHistogram(srcImp, 256, 0, Math.pow(2,ImagePlus.getDefault16bitRange())-1); + else + showHistogram(srcImp, 256); + } + } + synchronized(this) { + if (doUpdate) { + doUpdate = false; //and loop again + } else { + try {wait();} //notify wakes up the thread + catch(InterruptedException e) { //interrupted tells the thread to exit + return; + } + } + } + } + } + + private void createListeners() { + if (srcImp==null) + return; + ImagePlus.addImageListener(this); + Roi.addRoiListener(this); + if (live!=null) { + Font font = live.getFont(); + live.setFont(new Font(font.getName(), Font.BOLD, font.getSize())); + live.setForeground(Color.red); + } + } + + private void removeListeners() { + if (srcImp==null) + return; + ImagePlus.removeImageListener(this); + Roi.removeRoiListener(this); + if (live!=null) { + Font font = live.getFont(); + live.setFont(new Font(font.getName(), Font.PLAIN, font.getSize())); + live.setForeground(Color.black); + } + } + +} diff --git a/src/ij/gui/ImageCanvas.java b/src/ij/gui/ImageCanvas.java new file mode 100644 index 0000000..410dc1d --- /dev/null +++ b/src/ij/gui/ImageCanvas.java @@ -0,0 +1,1816 @@ +package ij.gui; + +import java.awt.*; +import java.util.Properties; +import java.awt.image.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.*; +import ij.plugin.frame.Recorder; +import ij.plugin.frame.RoiManager; +import ij.plugin.filter.Analyzer; +import ij.plugin.tool.PlugInTool; +import ij.macro.*; +import ij.*; +import ij.util.*; +import ij.text.*; +import java.awt.event.*; +import java.util.*; +import java.awt.geom.*; +import java.util.concurrent.atomic.AtomicBoolean; + + +/** This is a Canvas used to display images in a Window. */ +public class ImageCanvas extends Canvas implements MouseListener, MouseMotionListener, Cloneable { + + protected static Cursor defaultCursor = new Cursor(Cursor.DEFAULT_CURSOR); + protected static Cursor handCursor = new Cursor(Cursor.HAND_CURSOR); + protected static Cursor moveCursor = new Cursor(Cursor.MOVE_CURSOR); + protected static Cursor crosshairCursor = new Cursor(Cursor.CROSSHAIR_CURSOR); + + public static boolean usePointer = Prefs.usePointerCursor; + + protected ImagePlus imp; + protected boolean imageUpdated; + protected Rectangle srcRect; + protected int imageWidth, imageHeight; + protected int xMouse; // current cursor offscreen x location + protected int yMouse; // current cursor offscreen y location + + private boolean showCursorStatus = true; + private int sx2, sy2; + private boolean disablePopupMenu; + private static Color zoomIndicatorColor; + private static Font smallFont, largeFont; + private Font font; + private Rectangle[] labelRects; + private boolean maxBoundsReset; + private Overlay showAllOverlay; + private static final int LIST_OFFSET = 100000; + private static Color showAllColor = Prefs.getColor(Prefs.SHOW_ALL_COLOR, new Color(0, 255, 255)); + private Color defaultColor = showAllColor; + private static Color labelColor, bgColor; + private int resetMaxBoundsCount; + private Roi currentRoi; + private int mousePressedX, mousePressedY; + private long mousePressedTime; + private boolean overOverlayLabel; + + /** If the mouse moves less than this in screen pixels, successive zoom operations are on the same image pixel */ + protected final static int MAX_MOUSEMOVE_ZOOM = 10; + /** Screen coordinates where the last zoom operation was done (initialized to impossible value) */ + protected int lastZoomSX = -9999999; + protected int lastZoomSY = -9999999; + /** Image (=offscreen) coordinates where the cursor was moved to for zooming */ + protected int zoomTargetOX = -1; + protected int zoomTargetOY; + + protected ImageJ ij; + protected double magnification; + protected int dstWidth, dstHeight; + + protected int xMouseStart; + protected int yMouseStart; + protected int xSrcStart; + protected int ySrcStart; + protected int flags; + + private Image offScreenImage; + private int offScreenWidth = 0; + private int offScreenHeight = 0; + private boolean mouseExited = true; + private boolean customRoi; + private boolean drawNames; + private AtomicBoolean paintPending; + private boolean scaleToFit; + private boolean painted; + private boolean hideZoomIndicator; + private boolean flattening; + private Timer pressTimer; + private PopupMenu roiPopupMenu; + private static int longClickDelay = 1000; //ms + + + public ImageCanvas(ImagePlus imp) { + this.imp = imp; + paintPending = new AtomicBoolean(false); + ij = IJ.getInstance(); + int width = imp.getWidth(); + int height = imp.getHeight(); + imageWidth = width; + imageHeight = height; + srcRect = new Rectangle(0, 0, imageWidth, imageHeight); + setSize(imageWidth, imageHeight); + magnification = 1.0; + addMouseListener(this); + addMouseMotionListener(this); + addKeyListener(ij); // ImageJ handles keyboard shortcuts + setFocusTraversalKeysEnabled(false); + //setScaleToFit(true); + } + + void updateImage(ImagePlus imp) { + this.imp = imp; + int width = imp.getWidth(); + int height = imp.getHeight(); + imageWidth = width; + imageHeight = height; + srcRect = new Rectangle(0, 0, imageWidth, imageHeight); + setSize(imageWidth, imageHeight); + magnification = 1.0; + } + + /** Update this ImageCanvas to have the same zoom and scale settings as the one specified. */ + void update(ImageCanvas ic) { + if (ic==null || ic==this || ic.imp==null) + return; + if (ic.imp.getWidth()!=imageWidth || ic.imp.getHeight()!=imageHeight) + return; + srcRect = new Rectangle(ic.srcRect.x, ic.srcRect.y, ic.srcRect.width, ic.srcRect.height); + setMagnification(ic.magnification); + setSize(ic.dstWidth, ic.dstHeight); + } + + /** Sets the region of the image (in pixels) to be displayed. */ + public void setSourceRect(Rectangle r) { + if (r==null) + return; + r = new Rectangle(r.x, r.y, r.width, r.height); + imageWidth = imp.getWidth(); + imageHeight = imp.getHeight(); + if (r.x<0) r.x = 0; + if (r.y<0) r.y = 0; + if (r.width<1) + r.width = 1; + if (r.height<1) + r.height = 1; + if (r.width>imageWidth) + r.width = imageWidth; + if (r.height>imageHeight) + r.height = imageHeight; + if (r.x+r.width>imageWidth) + r.x = imageWidth-r.width; + if (r.y+r.height>imageHeight) + r.y = imageHeight-r.height; + if (srcRect==null) + srcRect = r; + else { + srcRect.x = r.x; + srcRect.y = r.y; + srcRect.width = r.width; + srcRect.height = r.height; + } + if (dstWidth==0) { + Dimension size = getSize(); + dstWidth = size.width; + dstHeight = size.height; + } + magnification = (double)dstWidth/srcRect.width; + imp.setTitle(imp.getTitle()); + if (IJ.debugMode) IJ.log("setSourceRect: "+magnification+" "+(int)(srcRect.height*magnification+0.5)+" "+dstHeight+" "+srcRect); + } + + void setSrcRect(Rectangle srcRect) { + setSourceRect(srcRect); + } + + public Rectangle getSrcRect() { + return srcRect; + } + + /** Obsolete; replaced by setSize() */ + public void setDrawingSize(int width, int height) { + dstWidth = width; + dstHeight = height; + setSize(dstWidth, dstHeight); + } + + public void setSize(int width, int height) { + super.setSize(width, height); + dstWidth = width; + dstHeight = height; + } + + /** ImagePlus.updateAndDraw calls this method to force the paint() + method to update the image from the ImageProcessor. */ + public void setImageUpdated() { + imageUpdated = true; + } + + public void setPaintPending(boolean state) { + paintPending.set(state); + } + + public boolean getPaintPending() { + return paintPending.get(); + } + + public void update(Graphics g) { + paint(g); + } + + //public void repaint() { + // super.repaint(); + // //if (IJ.debugMode) IJ.log("repaint: "+imp); + //} + + public void paint(Graphics g) { + // if (IJ.debugMode) IJ.log("paint: "+imp); + painted = true; + Roi roi = imp.getRoi(); + Overlay overlay = imp.getOverlay(); + if (roi!=null || overlay!=null || showAllOverlay!=null || Prefs.paintDoubleBuffered || (IJ.isLinux() && magnification<0.25)) { + // Use double buffering to avoid flickering of ROIs and to work around + // a Linux problem with large images not showing at low magnification. + if (roi!=null) + roi.updatePaste(); + if (imageWidth!=0) { + paintDoubleBuffered(g); + setPaintPending(false); + return; + } + } + try { + if (imageUpdated) { + imageUpdated = false; + imp.updateImage(); + } + setInterpolation(g, Prefs.interpolateScaledImages); + Image img = imp.getImage(); + if (img!=null) + g.drawImage(img, 0, 0, (int)(srcRect.width*magnification+0.5), (int)(srcRect.height*magnification+0.5), + srcRect.x, srcRect.y, srcRect.x+srcRect.width, srcRect.y+srcRect.height, null); + if (overlay!=null) + drawOverlay(overlay, g); + if (showAllOverlay!=null) + drawOverlay(showAllOverlay, g); + if (roi!=null) drawRoi(roi, g); + if (srcRect.width1.0) { + Object value = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; + ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_INTERPOLATION, value); + } + } + + private void drawRoi(Roi roi, Graphics g) { + if (Interpreter.isBatchMode()) + return; + if (roi==currentRoi) { + Color lineColor = roi.getStrokeColor(); + Color fillColor = roi.getFillColor(); + float lineWidth = roi.getStrokeWidth(); + roi.setStrokeColor(null); + roi.setFillColor(null); + boolean strokeSet = roi.getStroke()!=null; + if (strokeSet) + roi.setStrokeWidth(1); + roi.draw(g); + roi.setStrokeColor(lineColor); + if (strokeSet) + roi.setStrokeWidth(lineWidth); + roi.setFillColor(fillColor); + currentRoi = null; + } else + roi.draw(g); + } + + public int getSliceNumber(String label) { + if (label==null) return 0; + int slice = 0; + if (label.length()>=14 && label.charAt(4)=='-' && label.charAt(9)=='-') + slice = (int)Tools.parseDouble(label.substring(0,4),0); + else if (label.length()>=17 && label.charAt(5)=='-' && label.charAt(11)=='-') + slice = (int)Tools.parseDouble(label.substring(0,5),0); + else if (label.length()>=20 && label.charAt(6)=='-' && label.charAt(13)=='-') + slice = (int)Tools.parseDouble(label.substring(0,6),0); + return slice; + } + + private void drawOverlay(Overlay overlay, Graphics g) { + if (imp!=null && imp.getHideOverlay() && overlay!=showAllOverlay) + return; + flattening = imp!=null && ImagePlus.flattenTitle.equals(imp.getTitle()); + if (imp!=null && showAllOverlay!=null && overlay!=showAllOverlay) + overlay.drawLabels(false); + Color labelColor = overlay.getLabelColor(); + if (labelColor==null) labelColor = Color.white; + initGraphics(overlay, g, labelColor, Roi.getColor()); + int n = overlay.size(); + //if (IJ.debugMode) IJ.log("drawOverlay: "+n); + int currentImage = imp!=null?imp.getCurrentSlice():-1; + int stackSize = imp.getStackSize(); + if (stackSize==1) + currentImage = -1; + int channel=0, slice=0, frame=0; + boolean hyperstack = imp.isHyperStack(); + if (hyperstack) { + channel = imp.getChannel(); + slice = imp.getSlice(); + frame = imp.getFrame(); + } + drawNames = overlay.getDrawNames() && overlay.getDrawLabels(); + boolean drawLabels = drawNames || overlay.getDrawLabels(); + if (drawLabels) + labelRects = new Rectangle[n]; + else + labelRects = null; + font = overlay.getLabelFont(); + if (overlay.scalableLabels() && font!=null) { + double mag = getMagnification(); + if (mag!=1.0) + font = font.deriveFont((float)(font.getSize()*mag)); + } + Roi activeRoi = imp.getRoi(); + boolean roiManagerShowAllMode = overlay==showAllOverlay && !Prefs.showAllSliceOnly; + for (int i=0; i0) { + if (z==0 && imp.getNSlices()>1) + z = position; + else if (t==0) + t = position; + } + if (((c==0||c==channel) && (z==0||z==slice) && (t==0||t==frame)) || roiManagerShowAllMode) + drawRoi(g, roi, drawLabels?i+LIST_OFFSET:-1); + } else { + int position = stackSize>1?roi.getPosition():0; + if (position==0 && stackSize>1) + position = getSliceNumber(roi.getName()); + if (position>0 && imp.getCompositeMode()==IJ.COMPOSITE) + position = 0; + //IJ.log(position+" "+currentImage+" "+roiManagerShowAllMode); + if (position==0 || position==currentImage || roiManagerShowAllMode) + drawRoi(g, roi, drawLabels?i+LIST_OFFSET:-1); + } + } + ((Graphics2D)g).setStroke(Roi.onePixelWide); + drawNames = false; + font = null; + } + + void drawOverlay(Graphics g) { + drawOverlay(imp.getOverlay(), g); + } + + private void initGraphics(Overlay overlay, Graphics g, Color textColor, Color defaultColor) { + if (smallFont==null) { + smallFont = new Font("SansSerif", Font.PLAIN, 9); + largeFont = IJ.font12; + } + if (textColor!=null) { + labelColor = textColor; + if (overlay!=null && overlay.getDrawBackgrounds()) { + double brightness = (labelColor.getRed()+labelColor.getGreen()+labelColor.getBlue())/3.0; + if (labelColor==Color.green) brightness = 255; + bgColor = brightness<=85?Color.white:Color.black; + } else + bgColor = null; + } else { + int red = defaultColor.getRed(); + int green = defaultColor.getGreen(); + int blue = defaultColor.getBlue(); + if ((red+green+blue)/3<128) + labelColor = Color.white; + else + labelColor = Color.black; + bgColor = defaultColor; + } + this.defaultColor = defaultColor; + Font font = overlay!=null?overlay.getLabelFont():null; + if (font!=null && font.getSize()>12) + ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setColor(defaultColor); + } + + void drawRoi(Graphics g, Roi roi, int index) { + ImagePlus imp2 = roi.getImage(); + roi.setImage(imp); + Color saveColor = roi.getStrokeColor(); + if (saveColor==null) + roi.setStrokeColor(defaultColor); + if (roi.getStroke()==null) + ((Graphics2D)g).setStroke(Roi.onePixelWide); + if (roi instanceof TextRoi) + ((TextRoi)roi).drawText(g); + else + roi.drawOverlay(g); + roi.setStrokeColor(saveColor); + if (index>=0) { + if (roi==currentRoi) + g.setColor(Roi.getColor()); + else + g.setColor(defaultColor); + drawRoiLabel(g, index, roi); + } + if (imp2!=null) + roi.setImage(imp2); + else + roi.setImage(null); + } + + void drawRoiLabel(Graphics g, int index, Roi roi) { + if (roi.isCursor()) + return; + boolean pointRoi = roi instanceof PointRoi; + Rectangle r = roi.getBounds(); + int x = screenX(r.x); + int y = screenY(r.y); + double mag = getMagnification(); + int width = (int)(r.width*mag); + int height = (int)(r.height*mag); + int size = width>40 || height>40?12:9; + int pointSize = 0; + int crossSize = 0; + if (pointRoi) { + pointSize = ((PointRoi)roi).getSize(); + switch (pointSize) { + case 0: case 1: size=9; break; + case 2: case 3: size=10; break; + case 4: size=12; break; + } + crossSize = pointSize + 10 + 2*pointSize; + } + if (font!=null) { + g.setFont(font); + size = font.getSize(); + } else if (size==12) + g.setFont(largeFont); + else + g.setFont(smallFont); + boolean drawingList = index >= LIST_OFFSET; + if (drawingList) index -= LIST_OFFSET; + String label = "" + (index+1); + if (drawNames) + label = roi.getName(); + if (label==null) + return; + FontMetrics metrics = g.getFontMetrics(); + int w = metrics.stringWidth(label); + x = x + width/2 - w/2; + y = y + height/2 + Math.max(size/2,6); + int h = metrics.getAscent() + metrics.getDescent(); + int xoffset=0, yoffset=0; + if (pointRoi) { + xoffset = 6 + pointSize; + yoffset = h - 6 + pointSize; + } + if (bgColor!=null) { + int h2 = h; + if (font!=null && font.getSize()>14) + h2 = (int)(h2*0.8); + g.setColor(bgColor); + g.fillRoundRect(x-1+xoffset, y-h2+2+yoffset, w+1, h2-2, 5, 5); + } + if (labelRects!=null && index1.0) + w1 = (int)(w1/aspectRatio); + int h1 = (int)(w1*aspectRatio); + if (w1<4) w1 = 4; + if (h1<4) h1 = 4; + int w2 = (int)(w1*((double)srcRect.width/imageWidth)); + int h2 = (int)(h1*((double)srcRect.height/imageHeight)); + if (w2<1) w2 = 1; + if (h2<1) h2 = 1; + int x2 = (int)(w1*((double)srcRect.x/imageWidth)); + int y2 = (int)(h1*((double)srcRect.y/imageHeight)); + if (zoomIndicatorColor==null) + zoomIndicatorColor = new Color(128, 128, 255); + g.setColor(zoomIndicatorColor); + ((Graphics2D)g).setStroke(Roi.onePixelWide); + g.drawRect(x1, y1, w1, h1); + if (w2*h2<=200 || w2<10 || h2<10) + g.fillRect(x1+x2, y1+y2, w2, h2); + else + g.drawRect(x1+x2, y1+y2, w2, h2); + } + + // Use double buffer to reduce flicker when drawing complex ROIs. + // Author: Erik Meijering + void paintDoubleBuffered(Graphics g) { + final int srcRectWidthMag = (int)(srcRect.width*magnification+0.5); + final int srcRectHeightMag = (int)(srcRect.height*magnification+0.5); + if (offScreenImage==null || offScreenWidth!=srcRectWidthMag || offScreenHeight!=srcRectHeightMag) { + offScreenImage = createImage(srcRectWidthMag, srcRectHeightMag); + offScreenWidth = srcRectWidthMag; + offScreenHeight = srcRectHeightMag; + } + Roi roi = imp.getRoi(); + try { + if (imageUpdated) { + imageUpdated = false; + imp.updateImage(); + } + Graphics offScreenGraphics = offScreenImage.getGraphics(); + setInterpolation(offScreenGraphics, Prefs.interpolateScaledImages); + Image img = imp.getImage(); + if (img!=null) + offScreenGraphics.drawImage(img, 0, 0, srcRectWidthMag, srcRectHeightMag, + srcRect.x, srcRect.y, srcRect.x+srcRect.width, srcRect.y+srcRect.height, null); + Overlay overlay = imp.getOverlay(); + if (overlay!=null) + drawOverlay(overlay, offScreenGraphics); + if (showAllOverlay!=null) + drawOverlay(showAllOverlay, offScreenGraphics); + if (roi!=null) + drawRoi(roi, offScreenGraphics); + if (srcRect.widthfirstFrame+1000) { + firstFrame=System.currentTimeMillis(); + fps = frames; + frames=0; + } + g.setColor(Color.white); + g.fillRect(10, 12, 50, 15); + g.setColor(Color.black); + g.drawString((int)(fps+0.5) + " fps", 10, 25); + } + + public Dimension getPreferredSize() { + return new Dimension(dstWidth, dstHeight); + } + + /** Returns the current cursor location in image coordinates. */ + public Point getCursorLoc() { + return new Point(xMouse, yMouse); + } + + /** Returns 'true' if the cursor is over this image. */ + public boolean cursorOverImage() { + return !mouseExited; + } + + /** Returns the mouse event modifiers. */ + public int getModifiers() { + return flags; + } + + /** Returns the ImagePlus object that is associated with this ImageCanvas. */ + public ImagePlus getImage() { + return imp; + } + + /** Sets the cursor based on the current tool and cursor location. */ + public void setCursor(int sx, int sy, int ox, int oy) { + xMouse = ox; + yMouse = oy; + mouseExited = false; + Roi roi = imp.getRoi(); + ImageWindow win = imp.getWindow(); + overOverlayLabel = false; + if (win==null) + return; + if (IJ.spaceBarDown()) { + setCursor(handCursor); + return; + } + int id = Toolbar.getToolId(); + switch (id) { + case Toolbar.MAGNIFIER: + setCursor(moveCursor); + break; + case Toolbar.HAND: + setCursor(handCursor); + break; + default: //selection tool + PlugInTool tool = Toolbar.getPlugInTool(); + boolean arrowTool = roi!=null && (roi instanceof Arrow) && tool!=null && "Arrow Tool".equals(tool.getToolName()); + if ((id>=Toolbar.CUSTOM1) && !arrowTool) { + if (Prefs.usePointerCursor) + setCursor(defaultCursor); + else + setCursor(crosshairCursor); + } else if (roi!=null && roi.getState()!=roi.CONSTRUCTING && roi.isHandle(sx, sy)>=0) { + setCursor(handCursor); + } else if ((imp.getOverlay()!=null||showAllOverlay!=null) && overOverlayLabel(sx,sy,ox,oy) && (roi==null||roi.getState()!=roi.CONSTRUCTING)) { + overOverlayLabel = true; + setCursor(handCursor); + } else if (Prefs.usePointerCursor || (roi!=null && roi.getState()!=roi.CONSTRUCTING && roi.contains(ox, oy))) + setCursor(defaultCursor); + else + setCursor(crosshairCursor); + } + } + + private boolean overOverlayLabel(int sx, int sy, int ox, int oy) { + Overlay o = showAllOverlay; + if (o==null) + o = imp.getOverlay(); + if (o==null || !o.isSelectable() || !o.isDraggable()|| !o.getDrawLabels() || labelRects==null) + return false; + for (int i=o.size()-1; i>=0; i--) { + if (labelRects!=null&&labelRects[i]!=null&&labelRects[i].contains(sx,sy)) { + Roi roi = imp.getRoi(); + if (roi==null || !roi.contains(ox,oy)) + return true; + else + return false; + } + } + return false; + } + + /**Converts a screen x-coordinate to an offscreen x-coordinate (nearest pixel center).*/ + public int offScreenX(int sx) { + return srcRect.x + (int)(sx/magnification); + } + + /**Converts a screen y-coordinate to an offscreen y-coordinate (nearest pixel center).*/ + public int offScreenY(int sy) { + return srcRect.y + (int)(sy/magnification); + } + + /**Converts a screen x-coordinate to an offscreen x-coordinate (Roi coordinate of nearest pixel border).*/ + public int offScreenX2(int sx) { + return srcRect.x + (int)Math.round(sx/magnification); + } + + /**Converts a screen y-coordinate to an offscreen y-coordinate (Roi coordinate of nearest pixel border).*/ + public int offScreenY2(int sy) { + return srcRect.y + (int)Math.round(sy/magnification); + } + + /**Converts a screen x-coordinate to a floating-point offscreen x-coordinate.*/ + public double offScreenXD(int sx) { + return srcRect.x + sx/magnification; + } + + /**Converts a screen y-coordinate to a floating-point offscreen y-coordinate.*/ + public double offScreenYD(int sy) { + return srcRect.y + sy/magnification; + + } + + /**Converts an offscreen x-coordinate to a screen x-coordinate.*/ + public int screenX(int ox) { + return (int)((ox-srcRect.x)*magnification); + } + + /**Converts an offscreen y-coordinate to a screen y-coordinate.*/ + public int screenY(int oy) { + return (int)((oy-srcRect.y)*magnification); + } + + /**Converts a floating-point offscreen x-coordinate to a screen x-coordinate.*/ + public int screenXD(double ox) { + return (int)((ox-srcRect.x)*magnification); + } + + /**Converts a floating-point offscreen x-coordinate to a screen x-coordinate.*/ + public int screenYD(double oy) { + return (int)((oy-srcRect.y)*magnification); + } + + public double getMagnification() { + return magnification; + } + + public void setMagnification(double magnification) { + setMagnification2(magnification); + } + + void setMagnification2(double magnification) { + if (magnification>32.0) + magnification = 32.0; + if (magnificationdstWidth||height>dstHeight)&&win!=null&&win.maxBounds!=null&&width!=win.maxBounds.width-10) { + if (resetMaxBoundsCount!=0) + resetMaxBounds(); // Works around problem that prevented window from being larger than maximized size + resetMaxBoundsCount++; + } + if (scaleToFit || IJ.altKeyDown()) + {fitToWindow(); return;} + if (width>imageWidth*magnification) + width = (int)(imageWidth*magnification); + if (height>imageHeight*magnification) + height = (int)(imageHeight*magnification); + Dimension size = getSize(); + if (srcRect.widthimageWidth) + srcRect.x = imageWidth-srcRect.width; + if ((srcRect.y+srcRect.height)>imageHeight) + srcRect.y = imageHeight-srcRect.height; + repaint(); + } + //IJ.log("resizeCanvas2: "+srcRect+" "+dstWidth+" "+dstHeight+" "+width+" "+height); + } + + public void fitToWindow() { + ImageWindow win = imp.getWindow(); + if (win==null) return; + Rectangle bounds = win.getBounds(); + Insets insets = win.getInsets(); + int sliderHeight = win.getSliderHeight(); + double xmag = (double)(bounds.width-(insets.left+insets.right+ImageWindow.HGAP*2))/srcRect.width; + double ymag = (double)(bounds.height-(ImageWindow.VGAP*2+insets.top+insets.bottom+sliderHeight))/srcRect.height; + setMagnification(Math.min(xmag, ymag)); + int width=(int)(imageWidth*magnification); + int height=(int)(imageHeight*magnification); + if (width==dstWidth&&height==dstHeight) return; + srcRect=new Rectangle(0,0,imageWidth, imageHeight); + setSize(width, height); + getParent().doLayout(); + } + + void setMaxBounds() { + if (maxBoundsReset) { + maxBoundsReset = false; + ImageWindow win = imp.getWindow(); + if (win!=null && !IJ.isLinux() && win.maxBounds!=null) { + win.setMaximizedBounds(win.maxBounds); + win.setMaxBoundsTime = System.currentTimeMillis(); + } + } + } + + void resetMaxBounds() { + ImageWindow win = imp.getWindow(); + if (win!=null && (System.currentTimeMillis()-win.setMaxBoundsTime)>500L) { + win.setMaximizedBounds(win.maxWindowBounds); + maxBoundsReset = true; + } + } + + private static final double[] zoomLevels = { + 1/72.0, 1/48.0, 1/32.0, 1/24.0, 1/16.0, 1/12.0, + 1/8.0, 1/6.0, 1/4.0, 1/3.0, 1/2.0, 0.75, 1.0, 1.5, + 2.0, 3.0, 4.0, 6.0, 8.0, 12.0, 16.0, 24.0, 32.0 }; + + public static double getLowerZoomLevel(double currentMag) { + double newMag = zoomLevels[0]; + for (int i=0; i=0; i--) { + if (zoomLevels[i]>currentMag) + newMag = zoomLevels[i]; + else + break; + } + return newMag; + } + + /** Zooms in by making the window bigger. If it can't be made bigger, then makes + the source rectangle (srcRect) smaller and centers it on the position in the + image where the cursor was when zooming has started. + Note that sx and sy are screen coordinates. */ + public void zoomIn(int sx, int sy) { + if (magnification>=32) return; + scaleToFit = false; + boolean mouseMoved = sqr(sx-lastZoomSX) + sqr(sy-lastZoomSY) > MAX_MOUSEMOVE_ZOOM*MAX_MOUSEMOVE_ZOOM; + lastZoomSX = sx; + lastZoomSY = sy; + if (mouseMoved || zoomTargetOX<0) { + boolean cursorInside = sx >= 0 && sy >= 0 && sx < dstWidth && sy < dstHeight; + zoomTargetOX = offScreenX(cursorInside ? sx : dstWidth/2); //where to zoom, offscreen (image) coordinates + zoomTargetOY = offScreenY(cursorInside ? sy : dstHeight/2); + } + double newMag = getHigherZoomLevel(magnification); + int newWidth = (int)(imageWidth*newMag); + int newHeight = (int)(imageHeight*newMag); + Dimension newSize = canEnlarge(newWidth, newHeight); + if (newSize!=null) { + setSize(newSize.width, newSize.height); + if (newSize.width!=newWidth || newSize.height!=newHeight) + adjustSourceRect(newMag, zoomTargetOX, zoomTargetOY); + else + setMagnification(newMag); + imp.getWindow().pack(); + } else // can't enlarge window + adjustSourceRect(newMag, zoomTargetOX, zoomTargetOY); + repaint(); + if (srcRect.widthimageWidth) r.x = imageWidth-w; + if (r.y+h>imageHeight) r.y = imageHeight-h; + srcRect = r; + setMagnification(newMag); + //IJ.log("adjustSourceRect2: "+srcRect+" "+dstWidth+" "+dstHeight); + } + + /** Returns the size to which the window can be enlarged, or null if it can't be enlarged. + * newWidth, newHeight is the size needed for showing the full image + * at the magnification needed */ + protected Dimension canEnlarge(int newWidth, int newHeight) { + if (IJ.altKeyDown()) + return null; + ImageWindow win = imp.getWindow(); + if (win==null) return null; + Rectangle r1 = win.getBounds(); + Insets insets = win.getInsets(); + Point loc = getLocation(); + if (loc.x>insets.left+5 || loc.y>insets.top+5) { + r1.width = newWidth+insets.left+insets.right+ImageWindow.HGAP*2; + r1.height = newHeight+insets.top+insets.bottom+ImageWindow.VGAP*2+win.getSliderHeight(); + } else { + r1.width = r1.width - dstWidth + newWidth; + r1.height = r1.height - dstHeight + newHeight; + } + Rectangle max = GUI.getMaxWindowBounds(win); + boolean fitsHorizontally = r1.x+r1.width MAX_MOUSEMOVE_ZOOM*MAX_MOUSEMOVE_ZOOM; + lastZoomSX = sx; + lastZoomSY = sy; + if (mouseMoved || zoomTargetOX<0) { + boolean cursorInside = sx >= 0 && sy >= 0 && sx < dstWidth && sy < dstHeight; + zoomTargetOX = offScreenX(cursorInside ? sx : dstWidth/2); //where to zoom, offscreen (image) coordinates + zoomTargetOY = offScreenY(cursorInside ? sy : dstHeight/2); + } + double oldMag = magnification; + double newMag = getLowerZoomLevel(magnification); + double srcRatio = (double)srcRect.width/srcRect.height; + double imageRatio = (double)imageWidth/imageHeight; + double initialMag = imp.getWindow().getInitialMagnification(); + if (Math.abs(srcRatio-imageRatio)>0.05) { + double scale = oldMag/newMag; + int newSrcWidth = (int)Math.round(srcRect.width*scale); + int newSrcHeight = (int)Math.round(srcRect.height*scale); + if (newSrcWidth>imageWidth) newSrcWidth=imageWidth; + if (newSrcHeight>imageHeight) newSrcHeight=imageHeight; + int newSrcX = srcRect.x - (newSrcWidth - srcRect.width)/2; + int newSrcY = srcRect.y - (newSrcHeight - srcRect.height)/2; + if (newSrcX + newSrcWidth > imageWidth) newSrcX = imageWidth - newSrcWidth; + if (newSrcY + newSrcHeight > imageHeight) newSrcY = imageHeight - newSrcHeight; + if (newSrcX<0) newSrcX = 0; + if (newSrcY<0) newSrcY = 0; + srcRect = new Rectangle(newSrcX, newSrcY, newSrcWidth, newSrcHeight); + //IJ.log(newMag+" "+srcRect+" "+dstWidth+" "+dstHeight); + int newDstWidth = (int)(srcRect.width*newMag); + int newDstHeight = (int)(srcRect.height*newMag); + setMagnification(newMag); + setMaxBounds(); + //IJ.log(newDstWidth+" "+dstWidth+" "+newDstHeight+" "+dstHeight); + if (newDstWidthdstWidth) { + int w = (int)Math.round(dstWidth/newMag); + if (w*newMagimageWidth) r.x = imageWidth-w; + if (r.y+h>imageHeight) r.y = imageHeight-h; + srcRect = r; + setMagnification(newMag); + } else { + srcRect = new Rectangle(0, 0, imageWidth, imageHeight); + setSize((int)(imageWidth*newMag), (int)(imageHeight*newMag)); + setMagnification(newMag); + imp.getWindow().pack(); + } + setMaxBounds(); + repaint(); + } + + int sqr(int x) { + return x*x; + } + + /** Implements the Image/Zoom/Original Scale command. */ + public void unzoom() { + double imag = imp.getWindow().getInitialMagnification(); + if (magnification==imag) + return; + srcRect = new Rectangle(0, 0, imageWidth, imageHeight); + ImageWindow win = imp.getWindow(); + setSize((int)(imageWidth*imag), (int)(imageHeight*imag)); + setMagnification(imag); + setMaxBounds(); + win.pack(); + setMaxBounds(); + repaint(); + } + + /** Implements the Image/Zoom/View 100% command. */ + public void zoom100Percent() { + if (magnification==1.0) + return; + double imag = imp.getWindow().getInitialMagnification(); + if (magnification!=imag) + unzoom(); + if (magnification==1.0) + return; + if (magnification<1.0) { + while (magnification<1.0) + zoomIn(imageWidth/2, imageHeight/2); + } else if (magnification>1.0) { + while (magnification>1.0) + zoomOut(imageWidth/2, imageHeight/2); + } else + return; + int x=xMouse, y=yMouse; + if (mouseExited) { + x = imageWidth/2; + y = imageHeight/2; + } + int sx = screenX(x); + int sy = screenY(y); + adjustSourceRect(1.0, sx, sy); + repaint(); + } + + protected void scroll(int sx, int sy) { + int ox = xSrcStart + (int)(sx/magnification); //convert to offscreen coordinates + int oy = ySrcStart + (int)(sy/magnification); + //IJ.log("scroll: "+ox+" "+oy+" "+xMouseStart+" "+yMouseStart); + int newx = xSrcStart + (xMouseStart-ox); + int newy = ySrcStart + (yMouseStart-oy); + if (newx<0) newx = 0; + if (newy<0) newy = 0; + if ((newx+srcRect.width)>imageWidth) newx = imageWidth-srcRect.width; + if ((newy+srcRect.height)>imageHeight) newy = imageHeight-srcRect.height; + srcRect.x = newx; + srcRect.y = newy; + //IJ.log(sx+" "+sy+" "+newx+" "+newy+" "+srcRect); + imp.draw(); + Thread.yield(); + } + + Color getColor(int index){ + IndexColorModel cm = (IndexColorModel)imp.getProcessor().getColorModel(); + return new Color(cm.getRGB(index)); + } + + /** Sets the foreground drawing color (or background color if + 'setBackground' is true) to the color of the pixel at (ox,oy). */ + public void setDrawingColor(int ox, int oy, boolean setBackground) { + //IJ.log("setDrawingColor: "+setBackground+this); + int type = imp.getType(); + int[] v = imp.getPixel(ox, oy); + switch (type) { + case ImagePlus.GRAY8: { + if (setBackground) + setBackgroundColor(getColor(v[0])); + else + setForegroundColor(getColor(v[0])); + break; + } + case ImagePlus.GRAY16: case ImagePlus.GRAY32: { + double min = imp.getProcessor().getMin(); + double max = imp.getProcessor().getMax(); + double value = (type==ImagePlus.GRAY32)?Float.intBitsToFloat(v[0]):v[0]; + int index = (int)(255.0*((value-min)/(max-min))); + if (index<0) index = 0; + if (index>255) index = 255; + if (setBackground) + setBackgroundColor(getColor(index)); + else + setForegroundColor(getColor(index)); + break; + } + case ImagePlus.COLOR_RGB: case ImagePlus.COLOR_256: { + Color c = new Color(v[0], v[1], v[2]); + if (setBackground) + setBackgroundColor(c); + else + setForegroundColor(c); + break; + } + } + Color c; + if (setBackground) + c = Toolbar.getBackgroundColor(); + else { + c = Toolbar.getForegroundColor(); + imp.setColor(c); + } + IJ.showStatus("("+c.getRed()+", "+c.getGreen()+", "+c.getBlue()+")"); + } + + private void setForegroundColor(Color c) { + Toolbar.setForegroundColor(c); + if (Recorder.record) + Recorder.record("setForegroundColor", c.getRed(), c.getGreen(), c.getBlue()); + } + + private void setBackgroundColor(Color c) { + Toolbar.setBackgroundColor(c); + if (Recorder.record) + Recorder.record("setBackgroundColor", c.getRed(), c.getGreen(), c.getBlue()); + } + + public void mousePressed(final MouseEvent e) { + showCursorStatus = true; + int toolID = Toolbar.getToolId(); + ImageWindow win = imp.getWindow(); + if (win!=null && win.running2 && toolID!=Toolbar.MAGNIFIER) { + if (win instanceof StackWindow) + ((StackWindow)win).setAnimate(false); + else + win.running2 = false; + return; + } + + int x = e.getX(); + int y = e.getY(); + flags = e.getModifiers(); + if (toolID!=Toolbar.MAGNIFIER && (e.isPopupTrigger()||(!IJ.isMacintosh()&&(flags&Event.META_MASK)!=0))) { + handlePopupMenu(e); + return; + } + + int ox = offScreenX(x); + int oy = offScreenY(y); + xMouse = ox; yMouse = oy; + if (IJ.spaceBarDown()) { + // temporarily switch to "hand" tool of space bar down + setupScroll(ox, oy); + return; + } + + if (overOverlayLabel && (imp.getOverlay()!=null||showAllOverlay!=null)) { + if (activateOverlayRoi(ox, oy)) + return; + } + + if ((System.currentTimeMillis()-mousePressedTime)<300L && !drawingTool()) { + if (activateOverlayRoi(ox,oy)) + return; + } + + mousePressedX = ox; + mousePressedY = oy; + mousePressedTime = System.currentTimeMillis(); + + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) { + tool.mousePressed(imp, e); + if (e.isConsumed()) return; + } + if (customRoi && imp.getOverlay()!=null) + return; + + if (toolID>=Toolbar.CUSTOM1) { + if (tool!=null && "Arrow Tool".equals(tool.getToolName())) + handleRoiMouseDown(e); + else + Toolbar.getInstance().runMacroTool(toolID); + return; + } + + final Roi roi1 = imp.getRoi(); + final int size1 = roi1!=null?roi1.size():0; + final Rectangle r1 = roi1!=null?roi1.getBounds():null; + + switch (toolID) { + case Toolbar.MAGNIFIER: + if (IJ.shiftKeyDown()) + zoomToSelection(ox, oy); + else if ((flags & (Event.ALT_MASK|Event.META_MASK|Event.CTRL_MASK))!=0) { + zoomOut(x, y); + if (getMagnification()<1.0) + imp.repaintWindow(); + } else { + zoomIn(x, y); + if (getMagnification()<=1.0) + imp.repaintWindow(); + } + break; + case Toolbar.HAND: + setupScroll(ox, oy); + break; + case Toolbar.DROPPER: + setDrawingColor(ox, oy, IJ.altKeyDown()); + break; + case Toolbar.WAND: + double tolerance = WandToolOptions.getTolerance(); + Roi roi = imp.getRoi(); + if (roi!=null && (tolerance==0.0||imp.isThreshold()) && roi.contains(ox, oy)) { + Rectangle r = roi.getBounds(); + if (r.width==imageWidth && r.height==imageHeight) + imp.deleteRoi(); + else if (!e.isAltDown()) { + handleRoiMouseDown(e); + return; + } + } + if (roi!=null) { + int handle = roi.isHandle(x, y); + if (handle>=0) { + roi.mouseDownInHandle(handle, x, y); + return; + } + } + if (!imp.okToDeleteRoi()) + break; + setRoiModState(e, roi, -1); + String mode = WandToolOptions.getMode(); + if (Prefs.smoothWand) + mode = mode + " smooth"; + int npoints = IJ.doWand(imp, ox, oy, tolerance, mode); + if (Recorder.record && npoints>0) { + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.doWand(imp, "+ox+", "+oy+", "+tolerance+", \""+mode+"\");"); + else { + if (tolerance==0.0 && mode.equals("Legacy")) + Recorder.record("doWand", ox, oy); + else + Recorder.recordString("doWand("+ox+", "+oy+", "+tolerance+", \""+mode+"\");\n"); + } + } + break; + case Toolbar.OVAL: + if (Toolbar.getBrushSize()>0) + new RoiBrush(); + else + handleRoiMouseDown(e); + break; + default: //selection tool + handleRoiMouseDown(e); + } + + if (longClickDelay>0) { + if (pressTimer==null) + pressTimer = new java.util.Timer(); + final Point cursorLoc = getCursorLoc(); + pressTimer.schedule(new TimerTask() { + public void run() { + if (pressTimer != null) { + pressTimer.cancel(); + pressTimer = null; + } + Roi roi2 = imp.getRoi(); + int size2 = roi2!=null?roi2.size():0; + Rectangle r2 = roi2!=null?roi2.getBounds():null; + boolean empty = r2!=null&&r2.width==0&&r2.height==0; + int state = roi2!=null?roi2.getState():-1; + boolean unchanged = state!=Roi.MOVING_HANDLE && r1!=null && r2!=null && r2.x==r1.x + && r2.y==r1.y && r2.width==r1.width && r2.height==r1.height && size2==size1 + && !(size2>1&&state==Roi.CONSTRUCTING); + boolean cursorMoved = !getCursorLoc().equals(cursorLoc); + //IJ.log(size2+" "+empty+" "+unchanged+" "+state+" "+roi1+" "+roi2); + if ((roi1==null && (size2<=1||empty)) || unchanged) { + if (roi1==null) imp.deleteRoi(); + if (!cursorMoved && Toolbar.getToolId()!=Toolbar.HAND) + handlePopupMenu(e); + } + } + }, longClickDelay); + } + + } + + + + private boolean drawingTool() { + int id = Toolbar.getToolId(); + return id==Toolbar.POLYLINE || id==Toolbar.FREELINE || id>=Toolbar.CUSTOM1; + } + + void zoomToSelection(int x, int y) { + IJ.setKeyUp(IJ.ALL_KEYS); + String macro = + "args = split(getArgument);\n"+ + "x1=parseInt(args[0]); y1=parseInt(args[1]); flags=20;\n"+ + "while (flags&20!=0) {\n"+ + "getCursorLoc(x2, y2, z, flags);\n"+ + "if (x2>=x1) x=x1; else x=x2;\n"+ + "if (y2>=y1) y=y1; else y=y2;\n"+ + "makeRectangle(x, y, abs(x2-x1), abs(y2-y1));\n"+ + "wait(10);\n"+ + "}\n"+ + "run('To Selection');\n"; + new MacroRunner(macro, x+" "+y); + } + + protected void setupScroll(int ox, int oy) { + xMouseStart = ox; + yMouseStart = oy; + xSrcStart = srcRect.x; + ySrcStart = srcRect.y; + } + + protected void handlePopupMenu(MouseEvent e) { + if (disablePopupMenu) return; + if (IJ.debugMode) IJ.log("show popup: " + (e.isPopupTrigger()?"true":"false")); + int sx = e.getX(); + int sy = e.getY(); + int ox = offScreenX(sx); + int oy = offScreenY(sy); + Roi roi = imp.getRoi(); + if (roi!=null && (roi.getType()==Roi.POLYGON || roi.getType()==Roi.POLYLINE || roi.getType()==Roi.ANGLE) + && roi.getState()==roi.CONSTRUCTING) { + roi.handleMouseUp(sx, sy); // simulate double-click to finalize + roi.handleMouseUp(sx, sy); // polygon or polyline selection + return; + } + if (roi!=null && !(e.isAltDown()||e.isShiftDown())) { // show ROI popup? + if (roi.contains(ox,oy)) { + if (roiPopupMenu==null) + addRoiPopupMenu(); + if (IJ.isMacOSX()) IJ.wait(10); + roiPopupMenu.show(this, sx, sy); + return; + } + } + PopupMenu popup = Menus.getPopupMenu(); + if (popup!=null) { + add(popup); + if (IJ.isMacOSX()) IJ.wait(10); + popup.show(this, sx, sy); + } + } + + public void mouseExited(MouseEvent e) { + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) { + tool.mouseExited(imp, e); + if (e.isConsumed()) return; + } + ImageWindow win = imp.getWindow(); + if (win!=null) + setCursor(defaultCursor); + IJ.showStatus(""); + mouseExited = true; + } + + public void mouseDragged(MouseEvent e) { + int x = e.getX(); + int y = e.getY(); + xMouse = offScreenX(x); + yMouse = offScreenY(y); + flags = e.getModifiers(); + mousePressedX = mousePressedY = -1; + //IJ.log("mouseDragged: "+flags); + if (flags==0) // workaround for Mac OS 9 bug + flags = InputEvent.BUTTON1_MASK; + if (Toolbar.getToolId()==Toolbar.HAND || IJ.spaceBarDown()) + scroll(x, y); + else { + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) { + tool.mouseDragged(imp, e); + if (e.isConsumed()) return; + } + IJ.setInputEvent(e); + Roi roi = imp.getRoi(); + if (roi != null) + roi.handleMouseDrag(x, y, flags); + } + } + + protected void handleRoiMouseDown(MouseEvent e) { + int sx = e.getX(); + int sy = e.getY(); + int ox = offScreenX(sx); + int oy = offScreenY(sy); + Roi roi = imp.getRoi(); + int tool = Toolbar.getToolId(); + + int handle = roi!=null?roi.isHandle(sx, sy):-1; + boolean multiPointMode = roi!=null && (roi instanceof PointRoi) && handle==-1 + && tool==Toolbar.POINT && Toolbar.getMultiPointMode(); + if (multiPointMode) { + double oxd = roi.offScreenXD(sx); + double oyd = roi.offScreenYD(sy); + if (e.isShiftDown() && !IJ.isMacro()) { + FloatPolygon points = roi.getFloatPolygon(); + if (points.npoints>0) { + double x0 = points.xpoints[0]; + double y0 = points.ypoints[0]; + double slope = Math.abs((oxd-x0)/(oyd-y0)); + if (slope>=1.0) + oyd = points.ypoints[0]; + else + oxd = points.xpoints[0]; + } + } + ((PointRoi)roi).addUserPoint(imp, oxd, oyd); + imp.setRoi(roi); + return; + } + + if (roi!=null && (roi instanceof PointRoi)) { + int npoints = ((PolygonRoi)roi).getNCoordinates(); + if (npoints>1 && handle==-1 && !IJ.altKeyDown() && !(tool==Toolbar.POINT && !Toolbar.getMultiPointMode()&&IJ.shiftKeyDown())) { + String msg = "Type shift-a (Edit>Selection>Select None) to delete\npoints. Use multi-point tool to add points."; + GenericDialog gd=new GenericDialog("Point Selection"); + gd.addMessage(msg); + gd.addHelp(PointToolOptions.help); + gd.hideCancelButton(); + gd.showDialog(); + return; + } + } + + setRoiModState(e, roi, handle); + if (roi!=null) { + if (handle>=0) { + roi.mouseDownInHandle(handle, sx, sy); + return; + } + Rectangle r = roi.getBounds(); + int type = roi.getType(); + if (type==Roi.RECTANGLE && r.width==imp.getWidth() && r.height==imp.getHeight() + && roi.getPasteMode()==Roi.NOT_PASTING && !(roi instanceof ImageRoi)) { + imp.deleteRoi(); + return; + } + if (roi.contains(ox, oy)) { + if (roi.modState==Roi.NO_MODS) + roi.handleMouseDown(sx, sy); + else { + imp.deleteRoi(); + imp.createNewRoi(sx,sy); + } + return; + } + boolean segmentedTool = tool==Toolbar.POLYGON || tool==Toolbar.POLYLINE || tool==Toolbar.ANGLE; + if (segmentedTool && (type==Roi.POLYGON || type==Roi.POLYLINE || type==Roi.ANGLE) + && roi.getState()==roi.CONSTRUCTING) + return; + if (segmentedTool&& !(IJ.shiftKeyDown()||IJ.altKeyDown())) { + imp.deleteRoi(); + return; + } + } + imp.createNewRoi(sx,sy); + } + + void setRoiModState(MouseEvent e, Roi roi, int handle) { + if (roi==null || (handle>=0 && roi.modState==Roi.NO_MODS)) + return; + if (roi.state==Roi.CONSTRUCTING) + return; + int tool = Toolbar.getToolId(); + if (tool>Toolbar.FREEROI && tool!=Toolbar.WAND && tool!=Toolbar.POINT) + {roi.modState = Roi.NO_MODS; return;} + if (e.isShiftDown()) + roi.modState = Roi.ADD_TO_ROI; + else if (e.isAltDown()) + roi.modState = Roi.SUBTRACT_FROM_ROI; + else + roi.modState = Roi.NO_MODS; + //IJ.log("setRoiModState: "+roi.modState+" "+ roi.state); + } + + /** Disable/enable popup menu. */ + public void disablePopupMenu(boolean status) { + disablePopupMenu = status; + } + + public void setShowAllList(Overlay showAllList) { + this.showAllOverlay = showAllList; + labelRects = null; + } + + public Overlay getShowAllList() { + return showAllOverlay; + } + + /** Obsolete */ + public void setShowAllROIs(boolean showAllROIs) { + RoiManager rm = RoiManager.getInstance(); + if (rm!=null) + rm.runCommand(showAllROIs?"show all":"show none"); + } + + /** Obsolete */ + public boolean getShowAllROIs() { + return getShowAllList()!=null; + } + + /** Obsolete */ + public static Color getShowAllColor() { + if (showAllColor!=null && showAllColor.getRGB()==0xff80ffff) + showAllColor = Color.cyan; + return showAllColor; + } + + /** Obsolete */ + public static void setShowAllColor(Color c) { + if (c==null) return; + showAllColor = c; + labelColor = null; + } + + /** Experimental */ + public static void setCursor(Cursor cursor, int type) { + crosshairCursor = cursor; + } + + /** Use ImagePlus.setOverlay(ij.gui.Overlay). */ + public void setOverlay(Overlay overlay) { + imp.setOverlay(overlay); + } + + /** Use ImagePlus.getOverlay(). */ + public Overlay getOverlay() { + return imp.getOverlay(); + } + + /** + * @deprecated + * replaced by ImagePlus.setOverlay(ij.gui.Overlay) + */ + public void setDisplayList(Vector list) { + if (list!=null) { + Overlay list2 = new Overlay(); + list2.setVector(list); + imp.setOverlay(list2); + } else + imp.setOverlay(null); + Overlay overlay = imp.getOverlay(); + if (overlay!=null) + overlay.drawLabels(overlay.size()>0&&overlay.get(0).getStrokeColor()==null); + else + customRoi = false; + repaint(); + } + + /** + * @deprecated + * replaced by ImagePlus.setOverlay(Shape, Color, BasicStroke) + */ + public void setDisplayList(Shape shape, Color color, BasicStroke stroke) { + imp.setOverlay(shape, color, stroke); + } + + /** + * @deprecated + * replaced by ImagePlus.setOverlay(Roi, Color, int, Color) + */ + public void setDisplayList(Roi roi, Color color) { + roi.setStrokeColor(color); + Overlay list = new Overlay(); + list.add(roi); + imp.setOverlay(list); + } + + /** + * @deprecated + * replaced by ImagePlus.getOverlay() + */ + public Vector getDisplayList() { + Overlay overlay = imp.getOverlay(); + if (overlay==null) + return null; + Vector displayList = new Vector(); + for (int i=0; i250L && !drawingTool()) { // long press + if (activateOverlayRoi(ox,oy)) + return; + } + + } + + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) { + tool.mouseReleased(imp, e); + if (e.isConsumed()) return; + } + flags = e.getModifiers(); + flags &= ~InputEvent.BUTTON1_MASK; // make sure button 1 bit is not set + flags &= ~InputEvent.BUTTON2_MASK; // make sure button 2 bit is not set + flags &= ~InputEvent.BUTTON3_MASK; // make sure button 3 bit is not set + Roi roi = imp.getRoi(); + if (roi != null) { + Rectangle r = roi.getBounds(); + int type = roi.getType(); + if ((r.width==0 || r.height==0) + && !(type==Roi.POLYGON||type==Roi.POLYLINE||type==Roi.ANGLE||type==Roi.LINE) + && !(roi instanceof TextRoi) + && roi.getState()==roi.CONSTRUCTING + && type!=roi.POINT) + imp.deleteRoi(); + else + roi.handleMouseUp(e.getX(), e.getY()); + } + } + + private boolean activateOverlayRoi(int ox, int oy) { + int currentImage = -1; + int stackSize = imp.getStackSize(); + if (stackSize>1) + currentImage = imp.getCurrentSlice(); + int channel=0, slice=0, frame=0; + boolean hyperstack = imp.isHyperStack(); + if (hyperstack) { + channel = imp.getChannel(); + slice = imp.getSlice(); + frame = imp.getFrame(); + } + Overlay o = showAllOverlay; + if (o==null) + o = imp.getOverlay(); + if (o==null || !o.isSelectable()) + return false; + boolean roiManagerShowAllMode = o==showAllOverlay && !Prefs.showAllSliceOnly; + boolean labels = o.getDrawLabels(); + int sx = screenX(ox); + int sy = screenY(oy); + for (int i=o.size()-1; i>=0; i--) { + Roi roi = o.get(i); + if (roi==null) + continue; + //IJ.log(".isAltDown: "+roi.contains(ox, oy)); + boolean containsMousePoint = false; + if (roi instanceof Line) { //grab line roi near its center + double grabLineWidth = 1.1 + 5./magnification; + containsMousePoint = (((Line)roi).getFloatPolygon(grabLineWidth)).contains(ox, oy); + } else + containsMousePoint = roi.contains(ox, oy); + if (containsMousePoint || (labels&&labelRects!=null&&labelRects[i]!=null&&labelRects[i].contains(sx,sy))) { + if (hyperstack && roi.getPosition()==0) { + int c = roi.getCPosition(); + int z = roi.getZPosition(); + int t = roi.getTPosition(); + if (!((c==0||c==channel)&&(z==0||z==slice)&&(t==0||t==frame) || roiManagerShowAllMode)) + continue; + } else { + int position = stackSize>1?roi.getPosition():0; + if (!(position==0||position==currentImage||roiManagerShowAllMode)) + continue; + } + if (!IJ.altKeyDown() && roi.getType()==Roi.COMPOSITE + && roi.getBounds().width==imp.getWidth() && roi.getBounds().height==imp.getHeight()) + return false; + if (Toolbar.getToolId()==Toolbar.OVAL && Toolbar.getBrushSize()>0) + Toolbar.getInstance().setTool(Toolbar.RECTANGLE); + roi.setImage(null); + imp.setRoi(roi); + //roi.handleMouseDown(sx, sy); + roiManagerSelect(roi, false); + ResultsTable.selectRow(roi); + return true; + } + } + return false; + } + + public boolean roiManagerSelect(Roi roi, boolean delete) { + RoiManager rm=RoiManager.getInstance(); + if (rm==null) + return false; + int index = rm.getRoiIndex(roi); + if (index<0) + return false; + if (delete) { + rm.select(imp, index); + rm.runCommand("delete"); + } else + rm.selectAndMakeVisible(imp, index); + return true; + } + + public void mouseMoved(MouseEvent e) { + //if (ij==null) return; + int sx = e.getX(); + int sy = e.getY(); + int ox = offScreenX(sx); + int oy = offScreenY(sy); + flags = e.getModifiers(); + setCursor(sx, sy, ox, oy); + mousePressedX = mousePressedY = -1; + IJ.setInputEvent(e); + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) { + tool.mouseMoved(imp, e); + if (e.isConsumed()) return; + } + Roi roi = imp.getRoi(); + int type = roi!=null?roi.getType():-1; + if (type>0 && (type==Roi.POLYGON||type==Roi.POLYLINE||type==Roi.ANGLE||type==Roi.LINE) + && roi.getState()==roi.CONSTRUCTING) + roi.mouseMoved(e); + else { + if (ox144) + showCursorStatus = true; + if (win!=null&&showCursorStatus) + win.mouseMoved(ox, oy); + } else + IJ.showStatus(""); + } + } + + public void mouseEntered(MouseEvent e) { + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) + tool.mouseEntered(imp, e); + } + + public void mouseClicked(MouseEvent e) { + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) + tool.mouseClicked(imp, e); + } + + public void setScaleToFit(boolean scaleToFit) { + this.scaleToFit = scaleToFit; + } + + public boolean getScaleToFit() { + return scaleToFit; + } + + public boolean hideZoomIndicator(boolean hide) { + boolean hidden = this.hideZoomIndicator; + if (!(srcRect.width0) dim.height += vgap; + dim.height += d.height; + } + Insets insets = target.getInsets(); + dim.width += insets.left + insets.right + hgap*2; + dim.height += insets.top + insets.bottom + vgap*2; + return dim; + } + + /** Returns the minimum dimensions for this layout. */ + public Dimension minimumLayoutSize(Container target) { + return preferredLayoutSize(target); + } + + /** Determines whether to ignore the width of non-image components when calculating + * the preferred width (default false, i.e. the maximum of the widths of all components is used). + * When true, components that do not fit the window will be truncated at the right. + * The width of the 0th component (the ImageCanvas) is always taken into account. */ + public void ignoreNonImageWidths(boolean ignoreNonImageWidths) { + this.ignoreNonImageWidths = ignoreNonImageWidths; + } + + /** Centers the elements in the specified column, if there is any slack.*/ + private void moveComponents(Container target, int x, int y, int width, int height, int nmembers) { + int x2 = 0; + y += height / 2; + for (int i=0; i60) + x2 = x + (width - d.width)/2; + m.setLocation(x2, y); + y += vgap + d.height; + } + } + + /** Lays out the container and calls ImageCanvas.resizeCanvas() + to adjust the image canvas size as needed. */ + public void layoutContainer(Container target) { + Insets insets = target.getInsets(); + int nmembers = target.getComponentCount(); + Dimension d; + int extraHeight = 0; + for (int i=1; i 0) y += vgap; + y += d.height; + if (i==0 || !ignoreNonImageWidths) + colw = Math.max(colw, d.width); + } + moveComponents(target, x, insets.top + vgap, colw, maxheight - y, nmembers); + } + +} diff --git a/src/ij/gui/ImagePanel.java b/src/ij/gui/ImagePanel.java new file mode 100644 index 0000000..628e575 --- /dev/null +++ b/src/ij/gui/ImagePanel.java @@ -0,0 +1,28 @@ +package ij.gui; +import java.awt.*; +import ij.*; + +/** This class is used by GenericDialog to add images to dialogs. */ +public class ImagePanel extends Panel { + private ImagePlus img; + private int width, height; + + ImagePanel(ImagePlus img) { + this.img = img; + width = img.getWidth(); + height = img.getHeight(); + } + + public Dimension getPreferredSize() { + return new Dimension(width, height); + } + + public Dimension getMinimumSize() { + return new Dimension(width, height); + } + + public void paint(Graphics g) { + g.drawImage(img.getProcessor().createImage(), 0, 0, null); + } + +} diff --git a/src/ij/gui/ImageRoi.java b/src/ij/gui/ImageRoi.java new file mode 100644 index 0000000..024366d --- /dev/null +++ b/src/ij/gui/ImageRoi.java @@ -0,0 +1,150 @@ +package ij.gui; +import ij.ImagePlus; +import ij.process.*; +import ij.io.FileSaver; +import java.awt.*; +import java.awt.image.*; + +/** An ImageRoi is an Roi that overlays an image. +* @see ij.ImagePlus#setOverlay(ij.gui.Overlay) +*/ +public class ImageRoi extends Roi { + private Image img; + private Composite composite; + private double opacity = 1.0; + private double angle = 0.0; + private boolean zeroTransparent; + private ImageProcessor ip; + + /** Creates a new ImageRoi from a BufferedImage.*/ + public ImageRoi(int x, int y, BufferedImage bi) { + super(x, y, bi.getWidth(), bi.getHeight()); + img = bi; + setStrokeColor(Color.black); + } + + /** Creates a new ImageRoi from a ImageProcessor.*/ + public ImageRoi(int x, int y, ImageProcessor ip) { + super(x, y, ip.getWidth(), ip.getHeight()); + img = ip.createImage(); + this.ip = ip; + setStrokeColor(Color.black); + } + + public void draw(Graphics g) { + Graphics2D g2d = (Graphics2D)g; + double mag = getMagnification(); + int sx2 = screenX(x+width); + int sy2 = screenY(y+height); + Composite saveComposite = null; + if (composite!=null) { + saveComposite = g2d.getComposite(); + g2d.setComposite(composite); + } + Image img2 = img; + if (angle!=0.0) { + ImageProcessor ip = new ColorProcessor(img); + ip.setInterpolate(true); + ip.setBackgroundValue(0.0); + ip.rotate(angle); + if (zeroTransparent) + ip = makeZeroTransparent(ip, true); + img2 = ip.createImage(); + } + g.drawImage(img2, screenX(x), screenY(y), sx2, sy2, 0, 0, img.getWidth(null), img.getHeight(null), null); + if (composite!=null) g2d.setComposite(saveComposite); + if (isActiveOverlayRoi() && !overlay) + super.draw(g); + } + + /** Sets the composite mode. */ + public void setComposite(Composite composite) { + this.composite = composite; + } + + /** Sets the composite mode using the specified opacity (alpha), in the + range 0.0-1.0, where 0.0 is fully transparent and 1.0 is fully opaque. */ + public void setOpacity(double opacity) { + if (opacity<0.0) opacity = 0.0; + if (opacity>1.0) opacity = 1.0; + this.opacity = opacity; + if (opacity!=1.0) + composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float)opacity); + else + composite = null; + } + + /** Returns a serialized version of the image. */ + public byte[] getSerializedImage() { + ImagePlus imp = new ImagePlus("",img); + return new FileSaver(imp).serialize(); + } + + /** Returns the current opacity. */ + public double getOpacity() { + return opacity; + } + + public void rotate(double angle) { + this.angle += angle; + } + + public void setAngle(double angle) { + this.angle = angle; + } + + public void setZeroTransparent(boolean zeroTransparent) { + if (this.zeroTransparent!=zeroTransparent) { + ip = makeZeroTransparent(new ColorProcessor(img), zeroTransparent); + img = ip.createImage(); + } + this.zeroTransparent = zeroTransparent; + } + + public boolean getZeroTransparent() { + return zeroTransparent; + } + + private ImageProcessor makeZeroTransparent(ImageProcessor ip, boolean transparent) { + if (transparent) { + ip.setColorModel(new DirectColorModel(32,0x00ff0000,0x0000ff00,0x000000ff,0xff000000)); + for (int x=0; x1) + ip.set(x, y, ip.get(x,y)|0xff000000); // set alpha bits + else + ip.set(x, y, ip.get(x,y)&0xffffff); // clear alpha bits + } + } + } + return ip; + } + + public synchronized Object clone() { + ImageRoi roi2 = (ImageRoi)super.clone(); + ImagePlus imp = new ImagePlus("", img); + roi2.setProcessor(imp.getProcessor()); + roi2.setOpacity(getOpacity()); + roi2.zeroTransparent = !zeroTransparent; + roi2.setZeroTransparent(zeroTransparent); + return roi2; + } + + public ImageProcessor getProcessor() { + if (ip!=null) + return ip; + else { + ip = new ColorProcessor(img); + return ip; + } + } + + public void setProcessor(ImageProcessor ip) { + img = ip.createImage(); + this.ip = ip; + width = ip.getWidth(); + height = ip.getHeight(); + } + +} \ No newline at end of file diff --git a/src/ij/gui/ImageWindow.java b/src/ij/gui/ImageWindow.java new file mode 100644 index 0000000..bb1942a --- /dev/null +++ b/src/ij/gui/ImageWindow.java @@ -0,0 +1,763 @@ +package ij.gui; +import java.awt.*; +import java.awt.image.*; +import java.util.Properties; +import java.awt.event.*; +import ij.*; +import ij.process.*; +import ij.io.*; +import ij.measure.*; +import ij.plugin.frame.*; +import ij.plugin.PointToolOptions; +import ij.macro.Interpreter; +import ij.util.*; + +/** A frame for displaying images. */ +public class ImageWindow extends Frame implements FocusListener, WindowListener, WindowStateListener, MouseWheelListener { + + public static final int MIN_WIDTH = 128; + public static final int MIN_HEIGHT = 32; + public static final int HGAP = 5; + public static final int VGAP = 5; + public static final String LOC_KEY = "image.loc"; + + protected ImagePlus imp; + protected ImageJ ij; + protected ImageCanvas ic; + private double initialMagnification = 1; + private int newWidth, newHeight; + protected boolean closed; + private boolean newCanvas; + private boolean unzoomWhenMinimizing = true; + Rectangle maxWindowBounds; // largest possible window on this screen + Rectangle maxBounds; // Size of this window after it is maximized + long setMaxBoundsTime; + private int sliderHeight; + + private static final int XINC = 12; + private static final int YINC = 16; + private final double SCALE = Prefs.getGuiScale(); + private int TEXT_GAP = 11; + private static int xbase = -1; + private static int ybase; + private static int xloc; + private static int yloc; + private static int count; + private static boolean centerOnScreen; + private static Point nextLocation; + public static long setMenuBarTime; + private int textGap = centerOnScreen?0:TEXT_GAP; + private Point initialLoc; + private int screenHeight, screenWidth; + + + /** This variable is set false if the user presses the escape key or closes the window. */ + public boolean running; + + /** This variable is set false if the user clicks in this + window, presses the escape key, or closes the window. */ + public boolean running2; + + public ImageWindow(String title) { + super(title); + } + + public ImageWindow(ImagePlus imp) { + this(imp, null); + } + + public ImageWindow(ImagePlus imp, ImageCanvas ic) { + super(imp.getTitle()); + if (SCALE>1.0) { + TEXT_GAP = (int)(TEXT_GAP*SCALE); + textGap = centerOnScreen?0:TEXT_GAP; + } + if (Prefs.blackCanvas && getClass().getName().equals("ij.gui.ImageWindow")) { + setForeground(Color.white); + setBackground(Color.black); + } else { + setForeground(Color.black); + if (IJ.isLinux()) + setBackground(ImageJ.backgroundColor); + else + setBackground(Color.white); + } + boolean openAsHyperStack = imp.getOpenAsHyperStack(); + ij = IJ.getInstance(); + this.imp = imp; + if (ic==null) { + ic = (this instanceof PlotWindow) ? new PlotCanvas(imp) : new ImageCanvas(imp); + newCanvas=true; + } + this.ic = ic; + ImageWindow previousWindow = imp.getWindow(); + setLayout(new ImageLayout(ic)); + add(ic); + addFocusListener(this); + addWindowListener(this); + addWindowStateListener(this); + addKeyListener(ij); + setFocusTraversalKeysEnabled(false); + if (!(this instanceof StackWindow)) + addMouseWheelListener(this); + setResizable(true); + if (!(this instanceof HistogramWindow&&IJ.isMacro()&&Interpreter.isBatchMode())) { + WindowManager.addWindow(this); + imp.setWindow(this); + } + if (previousWindow!=null) { + if (newCanvas) + setLocationAndSize(false); + else + ic.update(previousWindow.getCanvas()); + Point loc = previousWindow.getLocation(); + setLocation(loc.x, loc.y); + if (!(this instanceof StackWindow || this instanceof PlotWindow)) { //layout now unless components will be added later + pack(); + if (IJ.isMacro()) + imp.setDeactivated(); //prepare for waitTillActivated (imp may have been activated before if it gets a new Window now) + show(); + } + if (ic.getMagnification()!=0.0) + imp.setTitle(imp.getTitle()); + boolean unlocked = imp.lockSilently(); + boolean changes = imp.changes; + imp.changes = false; + previousWindow.close(); + imp.changes = changes; + if (unlocked) + imp.unlock(); + if (this.imp!=null) + this.imp.setOpenAsHyperStack(openAsHyperStack); + WindowManager.setCurrentWindow(this); + } else { + setLocationAndSize(false); + if (ij!=null && !IJ.isMacintosh()) { + Image img = ij.getIconImage(); + if (img!=null) try { + setIconImage(img); + } catch (Exception e) {} + } + if (nextLocation!=null) + setLocation(nextLocation); + else if (centerOnScreen) + GUI.center(this); + nextLocation = null; + centerOnScreen = false; + if (Interpreter.isBatchMode() || (IJ.getInstance()==null&&this instanceof HistogramWindow)) { + WindowManager.setTempCurrentImage(imp); + Interpreter.addBatchModeImage(imp); + } else { + if (IJ.isMacro()) + imp.setDeactivated(); //prepare for waitTillActivated (imp may have been activated previously and gets a new Window now) + show(); + } + } + } + + private void setLocationAndSize(boolean updating) { + if (imp==null) + return; + int width = imp.getWidth(); + int height = imp.getHeight(); + + // load prefernces file location + Point loc = Prefs.getLocation(LOC_KEY); + Rectangle bounds = null; + if (loc!=null) { + bounds = GUI.getMaxWindowBounds(loc); + if (bounds!=null && (loc.x>bounds.x+bounds.width/3||loc.y>bounds.y+bounds.height/3) + && (loc.x+width>bounds.x+bounds.width||loc.y+height>bounds.y+bounds.height)) { + loc = null; + bounds = null; + } + } + // if loc not valid, use screen bounds of visible window (this) or of main window (ij) if not visible yet (updating == false) + Rectangle maxWindow = bounds!=null?bounds:GUI.getMaxWindowBounds(updating?this: ij); + + if (WindowManager.getWindowCount()<=1) + xbase = -1; + if (width>maxWindow.width/2 && xbase>maxWindow.x+5+XINC*6) + xbase = -1; + if (xbase==-1) { + count = 0; + if (loc!=null) { + xbase = loc.x; + ybase = loc.y; + } else if (ij!=null) { + Rectangle ijBounds = ij.getBounds(); + if (ijBounds.y-maxWindow.xmaxWindow.x+maxWindow.width) { + xbase = maxWindow.x+maxWindow.width - width - 10; + if (xbasescreenWidth || ybase+height*mag>=screenHeight) { + double mag2 = ImageCanvas.getLowerZoomLevel(mag); + if (mag2==mag) break; + mag = mag2; + } + } + + if (mag<1.0) { + initialMagnification = mag; + ic.setSize((int)(width*mag), (int)(height*mag)); + } + ic.setMagnification(mag); + if (y+height*mag>screenHeight) + y = ybase; + if (Prefs.open100Percent && ic.getMagnification()<1.0) { + while(ic.getMagnification()<1.0) + ic.zoomIn(0, 0); + setSize(Math.min(width, screenWidth-x), Math.min(height, screenHeight-y)); + validate(); + } else + pack(); + if (!updating) { + setLocation(x, y); + initialLoc = new Point(x,y); + } + } + + Rectangle getMaxWindow(int xloc, int yloc) { + return GUI.getMaxWindowBounds(new Point(xloc, yloc)); + } + + public double getInitialMagnification() { + return initialMagnification; + } + + /** Override Container getInsets() to make room for some text above the image. */ + public Insets getInsets() { + Insets insets = super.getInsets(); + if (imp==null) + return insets; + double mag = ic.getMagnification(); + int extraWidth = (int)((MIN_WIDTH - imp.getWidth()*mag)/2.0); + if (extraWidth<0) extraWidth = 0; + int extraHeight = (int)((MIN_HEIGHT - imp.getHeight()*mag)/2.0); + if (extraHeight<0) extraHeight = 0; + insets = new Insets(insets.top+textGap+extraHeight, insets.left+extraWidth, insets.bottom+extraHeight, insets.right+extraWidth); + return insets; + } + + /** Draws the subtitle. */ + public void drawInfo(Graphics g) { + if (imp==null) + return; + if (textGap!=0) { + Insets insets = super.getInsets(); + Color savec = null; + if (imp.isComposite()) { + CompositeImage ci = (CompositeImage)imp; + if (ci.getMode()==IJ.COMPOSITE) { + savec = g.getColor(); + Color c = ci.getChannelColor(); + if (Color.green.equals(c)) + c = new Color(0,180,0); + g.setColor(c); + } + } + Java2.setAntialiasedText(g, true); + if (SCALE>1.0) { + Font font = new Font("SansSerif", Font.PLAIN, (int)(12*SCALE)); + g.setFont(font); + } + g.drawString(createSubtitle(), insets.left+5, insets.top+TEXT_GAP); + if (savec!=null) + g.setColor(savec); + } + } + + /** Creates the subtitle. */ + public String createSubtitle() { + String s=""; + if (imp==null) + return s; + int stackSize = imp.getStackSize(); + if (stackSize>1) { + ImageStack stack = imp.getStack(); + int currentSlice = imp.getCurrentSlice(); + s += currentSlice+"/"+stackSize; + String label = stack.getShortSliceLabel(currentSlice); + if (label!=null && label.length()>0) { + if (imp.isHyperStack()) label = label.replace(';', ' '); + s += " (" + label + ")"; + } + if ((this instanceof StackWindow) && running2) { + return s; + } + s += "; "; + } else { + String label = imp.getProp("Slice_Label"); + if (label==null && imp.isStack()) + label = imp.getStack().getSliceLabel(1); + if (label!=null && label.length()>0) { + int newline = label.indexOf('\n'); + if (newline>0) + label = label.substring(0, newline); + int len = label.length(); + if (len>4 && label.charAt(len-4)=='.' && !Character.isDigit(label.charAt(len-1))) + label = label.substring(0,len-4); + if (label.length()>60) + label = label.substring(0, 60)+"..."; + s = "\""+label + "\"; "; + } + } + int type = imp.getType(); + Calibration cal = imp.getCalibration(); + if (cal.scaled()) { + boolean unitsMatch = cal.getXUnit().equals(cal.getYUnit()); + double cwidth = imp.getWidth()*cal.pixelWidth; + double cheight = imp.getHeight()*cal.pixelHeight; + int digits = Tools.getDecimalPlaces(cwidth, cheight); + if (digits>2) digits=2; + if (unitsMatch) { + s += IJ.d2s(cwidth,digits) + "x" + IJ.d2s(cheight,digits) + + " " + cal.getUnits() + " (" + imp.getWidth() + "x" + imp.getHeight() + "); "; + } else { + s += d2s(cwidth) + " " + cal.getXUnit() + " x " + + d2s(cheight) + " " + cal.getYUnit() + + " (" + imp.getWidth() + "x" + imp.getHeight() + "); "; + } + } else + s += imp.getWidth() + "x" + imp.getHeight() + " pixels; "; + switch (type) { + case ImagePlus.GRAY8: + case ImagePlus.COLOR_256: + s += "8-bit"; + break; + case ImagePlus.GRAY16: + s += "16-bit"; + break; + case ImagePlus.GRAY32: + s += "32-bit"; + break; + case ImagePlus.COLOR_RGB: + s += imp.isRGB() ? "RGB" : "32-bit (int)"; + break; + } + if (imp.isInvertedLut()) + s += " (inverting LUT)"; + return s+"; "+getImageSize(imp); + } + + public static String getImageSize(ImagePlus imp) { + if (imp==null) + return null; + double size = imp.getSizeInBytes()/1024.0; + String s2=null, s3=null; + if (size<1024.0) + {s2=IJ.d2s(size,0); s3="K";} + else if (size<10000.0) + {s2=IJ.d2s(size/1024.0,1); s3="MB";} + else if (size<1048576.0) + {s2=IJ.d2s(Math.round(size/1024.0),0); s3="MB";} + else + {s2=IJ.d2s(size/1048576.0,1); s3="GB";} + if (s2.endsWith(".0")) s2 = s2.substring(0, s2.length()-2); + return s2+s3; + } + + private String d2s(double n) { + int digits = Tools.getDecimalPlaces(n); + if (digits>2) digits=2; + return IJ.d2s(n,digits); + } + + public void paint(Graphics g) { + drawInfo(g); + Rectangle r = ic.getBounds(); + int extraWidth = MIN_WIDTH - r.width; + int extraHeight = MIN_HEIGHT - r.height; + if (extraWidth<=0 && extraHeight<=0 && !Prefs.noBorder && !IJ.isLinux()) + g.drawRect(r.x-1, r.y-1, r.width+1, r.height+1); + } + + /** Removes this window from the window list and disposes of it. + Returns false if the user cancels the "save changes" dialog. */ + public boolean close() { + boolean isRunning = running || running2; + running = running2 = false; + if (imp==null) return true; + boolean virtual = imp.getStackSize()>1 && imp.getStack().isVirtual(); + if (isRunning) IJ.wait(500); + if (imp==null) return true; + boolean changes = imp.changes; + Roi roi = imp.getRoi(); + if (roi!=null && (roi instanceof PointRoi) && ((PointRoi)roi).promptBeforeDeleting()) + changes = true; + if (ij==null || ij.quittingViaMacro() || IJ.getApplet()!=null || Interpreter.isBatchMode() || IJ.macroRunning() || virtual) + changes = false; + if (changes) { + String msg; + String name = imp.getTitle(); + if (name.length()>22) + msg = "Save changes to\n" + "\"" + name + "\"?"; + else + msg = "Save changes to \"" + name + "\"?"; + if (imp.isLocked()) + msg += "\nWARNING: This image is locked.\nProbably, processing is unfinished (slow or still previewing)."; + toFront(); + YesNoCancelDialog d = new YesNoCancelDialog(this, "ImageJ", msg); + if (d.cancelPressed()) + return false; + else if (d.yesPressed()) { + FileSaver fs = new FileSaver(imp); + if (!fs.save()) return false; + } + } + closed = true; + if (WindowManager.getWindowCount()==0) { + xloc = 0; + yloc = 0; + } + WindowManager.removeWindow(this); + if (ij!=null && ij.quitting()) // this may help avoid thread deadlocks + return true; + Rectangle bounds = getBounds(); + if (initialLoc!=null && !bounds.equals(initialLoc) && !IJ.isMacro() + && bounds.y0) + sw.removeScrollbars(); + else if (stackSize>1 && nScrollbars==0) + sw.addScrollbars(imp); + } + pack(); + repaint(); + maxBounds = getMaximumBounds(); + setMaximizedBounds(maxBounds); + setMaxBoundsTime = System.currentTimeMillis(); + } + + public ImageCanvas getCanvas() { + return ic; + } + + + static ImagePlus getClipboard() { + return ImagePlus.getClipboard(); + } + + public Rectangle getMaximumBounds() { + Rectangle maxWindow = GUI.getMaxWindowBounds(this); + if (imp==null) + return maxWindow; + double width = imp.getWidth(); + double height = imp.getHeight(); + double iAspectRatio = width/height; + maxWindowBounds = maxWindow; + if (iAspectRatio/((double)maxWindow.width/maxWindow.height)>0.75) { + maxWindow.y += 22; // uncover ImageJ menu bar + maxWindow.height -= 22; + } + Dimension extraSize = getExtraSize(); + double maxWidth = maxWindow.width-extraSize.width; + double maxHeight = maxWindow.height-extraSize.height; + double mAspectRatio = maxWidth/maxHeight; + int wWidth, wHeight; + double mag; + if (iAspectRatio>=mAspectRatio) { + mag = maxWidth/width; + wWidth = maxWindow.width; + wHeight = (int)(height*mag+extraSize.height); + } else { + mag = maxHeight/height; + wHeight = maxWindow.height; + wWidth = (int)(width*mag+extraSize.width); + } + int xloc = (int)(maxWidth-wWidth)/2; + if (xlocwidth) srcRect.x = width-srcRect.width; + } else { + srcRect.y += rotation*amount*Math.max(height/200, 1); + if (srcRect.y<0) srcRect.y = 0; + if (srcRect.y+srcRect.height>height) srcRect.y = height-srcRect.height; + } + if (srcRect.x!=xstart || srcRect.y!=ystart) + ic.repaint(); + } + + /** Copies the current ROI to the clipboard. The entire + image is copied if there is no ROI. */ + public void copy(boolean cut) { + imp.copy(cut); + } + + + public void paste() { + imp.paste(); + } + + /** This method is called by ImageCanvas.mouseMoved(MouseEvent). + @see ij.gui.ImageCanvas#mouseMoved + */ + public void mouseMoved(int x, int y) { + imp.mouseMoved(x, y); + } + + public String toString() { + return imp!=null?imp.getTitle():""; + } + + /** Causes the next image to be opened to be centered on the screen + and displayed without informational text above the image. */ + public static void centerNextImage() { + centerOnScreen = true; + } + + /** Causes the next image to be displayed at the specified location. */ + public static void setNextLocation(Point loc) { + nextLocation = loc; + } + + /** Causes the next image to be displayed at the specified location. */ + public static void setNextLocation(int x, int y) { + nextLocation = new Point(x, y); + } + + /** Moves and resizes this window. Changes the + magnification so the image fills the window. */ + public void setLocationAndSize(int x, int y, int width, int height) { + setBounds(x, y, width, height); + getCanvas().fitToWindow(); + initialLoc = null; + pack(); + } + + @Override + public void setLocation(int x, int y) { + super.setLocation(x, y); + initialLoc = null; + } + + public void setSliderHeight(int height) { + sliderHeight = height; + } + + public int getSliderHeight() { + return sliderHeight; + } + + public static void setImageJMenuBar(ImageWindow win) { + ImageJ ij = IJ.getInstance(); + boolean setMenuBar = true; + ImagePlus imp = win.getImagePlus(); + if (imp!=null) + setMenuBar = imp.setIJMenuBar(); + MenuBar mb = Menus.getMenuBar(); + if (mb!=null && mb==win.getMenuBar()) + setMenuBar = false; + setMenuBarTime = 0L; + if (setMenuBar && ij!=null && !ij.quitting() && !Interpreter.nonBatchMacroRunning()) { + IJ.wait(10); // may be needed for Java 1.4 on OS X + long t0 = System.currentTimeMillis(); + win.setMenuBar(mb); + long time = System.currentTimeMillis()-t0; + setMenuBarTime = time; + Menus.setMenuBarCount++; + if (IJ.debugMode) IJ.log("setMenuBar: "+time+"ms ("+Menus.setMenuBarCount+")"); + if (time>2000L) + Prefs.setIJMenuBar = false; + } + if (imp!=null) imp.setIJMenuBar(true); + } + +} //class ImageWindow diff --git a/src/ij/gui/Line.java b/src/ij/gui/Line.java new file mode 100644 index 0000000..b4c601f --- /dev/null +++ b/src/ij/gui/Line.java @@ -0,0 +1,738 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.Straightener; +import ij.plugin.frame.Recorder; +import ij.plugin.CalibrationBar; +import java.awt.*; +import java.awt.image.*; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.awt.event.*; +import java.awt.geom.*; + +/** This class represents a straight line selection. */ +public class Line extends Roi { + public int x1, y1, x2, y2; // the line end points as integer coordinates, for compatibility only + public double x1d, y1d, x2d, y2d; // the line using sub-pixel coordinates + protected double x1R, y1R, x2R, y2R; // the line, relative to base of subpixel bounding rect 'bounds' + protected double startxd, startyd; + static boolean widthChanged; + private boolean dragged; + private int mouseUpCount; + + /** Creates a new straight line selection using the specified + starting and ending offscreen integer coordinates. */ + public Line(int ox1, int oy1, int ox2, int oy2) { + this((double)ox1, (double)oy1, (double)ox2, (double)oy2); + } + + /** Creates a new straight line selection using the specified + starting and ending offscreen double coordinates. */ + public Line(double ox1, double oy1, double ox2, double oy2) { + super((int)(ox1+0.5), (int)(oy1+0.5), 0, 0); + type = LINE; + updateCoordinates(ox1, oy1, ox2, oy2); + if (!(this instanceof Arrow) && lineWidth>1) + updateWideLine(lineWidth); + updateClipRect(); + oldX=x; oldY=y; oldWidth=width; oldHeight=height; + state = NORMAL; + } + + /** Creates a new straight line selection using the specified + starting and ending offscreen coordinates. */ + public static Line create(double x1, double y1, double x2, double y2) { + return new Line(x1, y1, x2, y2); + } + + /** Starts the process of creating a new user-generated straight line + selection. 'sx' and 'sy' are screen coordinates that specify + the start of the line. The user will determine the end of the line + interactively using rubber banding. */ + public Line(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + type = LINE; + startxd = offScreenXD(sx); + startyd = offScreenYD(sy); + if (!magnificationForSubPixel()) { + startxd = Math.round(startxd); + startyd = Math.round(startyd); + } + updateCoordinates(startxd, startyd, startxd, startyd); + if (!(this instanceof Arrow) && lineWidth>1) + updateWideLine(lineWidth); + } + + /** + * @deprecated + * replaced by Line(int, int, int, int) + */ + public Line(int ox1, int oy1, int ox2, int oy2, ImagePlus imp) { + this(ox1, oy1, ox2, oy2); + setImage(imp); + } + + protected void grow(int sx, int sy) { //mouseDragged + drawLine(sx, sy); + dragged = true; + } + + public void mouseMoved(MouseEvent e) { + drawLine(e.getX(), e.getY()); + } + + protected void handleMouseUp(int screenX, int screenY) { + mouseUpCount++; + if (Prefs.enhancedLineTool && mouseUpCount==1 && !dragged) + return; + state = NORMAL; + if (imp==null) return; + imp.draw(clipX-5, clipY-5, clipWidth+10, clipHeight+10); + if (Recorder.record) { + String method = (this instanceof Arrow)?"makeArrow":"makeLine"; + Recorder.record(method, x1, y1, x2, y2); + } + if (getLength()==0.0) + imp.deleteRoi(); + } + + protected void drawLine(int sx, int sy) { + double xend = offScreenXD(sx); + double yend = offScreenYD(sy); + if (xend<0.0) xend=0.0; if (yend<0.0) yend=0.0; + if (xend>xMax) xend=xMax; if (yend>yMax) yend=yMax; + double xstart=getXBase()+x1R, ystart=getYBase()+y1R; + if (constrain) { + int i=0; + double dy = Math.abs(yend-ystart); + double dx = Math.abs(xend-xstart); + double comp = dy / dx; + for (;i ystart) { + yend = ystart + dx*PI_MULT[i]; + } else { + yend = ystart - dx*PI_MULT[i]; + } + } else { + xend = xstart; + } + } + if (!magnificationForSubPixel() || IJ.controlKeyDown()) { //during creation, CTRL enforces integer coordinates + xstart=Math.round(xstart); ystart=Math.round(ystart); + xend=Math.round(xend); yend=Math.round(yend); + } + updateCoordinates(xstart, ystart, xend, yend); + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX=x; oldY=y; + oldWidth=width; oldHeight=height; + } + + /** Used for angle searches in line ROI creation: tan = y/x for angle limits 1/2*45 degrees, and 3/2*45 deg */ + private static final double[] PI_SEARCH = {Math.tan(Math.PI/8), Math.tan((3*Math.PI)/8)}; + private static final double[] PI_MULT = {0, 1}; // y/x for horizontal (0 degrees) and 45 deg + + void move(int sx, int sy) { + int xNew = offScreenX(sx); + int yNew = offScreenY(sy); + x += xNew - startxd; + y += yNew - startyd; + clipboard=null; + startxd = xNew; + startyd = yNew; + updateClipRect(); + if (ignoreClipRect) + imp.draw(); + else + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; + oldY = y; + oldWidth = width; + oldHeight=height; + } + + protected void moveHandle(int sx, int sy) { + if (constrain && activeHandle == 2) { // constrain translation in 90deg steps + int dx = sx - previousSX; + int dy = sy - previousSY; + if (Math.abs(dx) > Math.abs(dy)) + dy = 0; + else + dx = 0; + sx = previousSX + dx; + sy = previousSY + dy; + } + double ox = offScreenXD(sx); + double oy = offScreenYD(sy); + double x1d=getXBase()+x1R, y1d=getYBase()+y1R; + double x2d=getXBase()+x2R, y2d=getYBase()+y2R; + double length = Math.sqrt(sqr(x2d-x1d) + sqr(y2d-y1d)); + switch (activeHandle) { + case 0: + double dx = ox-x1d; + double dy = oy-y1d; + x1d=ox; + y1d=oy; + if(center){ + x2d -= dx; + y2d -= dy; + } + if (aspect){ + double ratio = length/(Math.sqrt(sqr(x2d-x1d) + sqr(y2d-y1d))); + double xcd = x1d+(x2d-x1d)/2; + double ycd = y1d+(y2d-y1d)/2; + + if(center){ + x1d=xcd-ratio*(xcd-x1d); + x2d=xcd+ratio*(x2d-xcd); + y1d=ycd-ratio*(ycd-y1d); + y2d=ycd+ratio*(y2d-ycd); + } else { + x1d=x2d-ratio*(x2d-x1d); + y1d=y2d-ratio*(y2d-y1d); + } + + } + break; + case 1: + dx = ox-x2d; + dy = oy-y2d; + x2d=ox; + y2d=oy; + if(center){ + x1d -= dx; + y1d -= dy; + } + if(aspect){ + double ratio = length/(Math.sqrt((x2d-x1d)*(x2d-x1d) + (y2d-y1d)*(y2d-y1d))); + double xcd = x1d+(x2d-x1d)/2; + double ycd = y1d+(y2d-y1d)/2; + + if(center){ + x1d=xcd-ratio*(xcd-x1d); + x2d=xcd+ratio*(x2d-xcd); + y1d=ycd-ratio*(ycd-y1d); + y2d=ycd+ratio*(y2d-ycd); + } else { + x2d=x1d+ratio*(x2d-x1d); + y2d=y1d+ratio*(y2d-y1d); + } + + } + break; + case 2: + dx = ox-(x1d+(x2d-x1d)/2); + dy = oy-(y1d+(y2d-y1d)/2); + x1d+=dx; y1d+=dy; x2d+=dx; y2d+=dy; + break; + } + if (constrain) { + double dx = Math.abs(x1d-x2d); + double dy = Math.abs(y1d-y2d); + double xcd = Math.min(x1d,x2d)+dx/2; + double ycd = Math.min(y1d,y2d)+dy/2; + + //double ratio = length/(Math.sqrt((x2d-x1d)*(x2d-x1d) + (y2d-y1d)*(y2d-y1d))); + if (activeHandle==0) { + if (dx>=dy) { + if(aspect){ + if(x2d>x1d) x1d=x2d-length; + else x1d=x2d+length; + } + y1d = y2d; + if(center) { + y1d=y2d=ycd; + if(aspect){ + if(xcd>x1d) { + x1d=xcd-length/2; + x2d=xcd+length/2; + } + else{ + x1d=xcd+length/2; + x2d=xcd-length/2; + } + } + } + } else { + if(aspect){ + if(y2d>y1d) y1d=y2d-length; + else y1d=y2d+length; + } + x1d = x2d; + if(center){ + x1d=x2d=xcd; + if(aspect){ + if(ycd>y1d) { + y1d=ycd-length/2; + y2d=ycd+length/2; + } + else{ + y1d=ycd+length/2; + y2d=ycd-length/2; + } + } + } + } + } else if (activeHandle==1) { + if (dx>=dy) { + if(aspect){ + if(x1d>x2d) x2d=x1d-length; + else x2d=x1d+length; + } + y2d= y1d; + if(center){ + y1d=y2d=ycd; + if(aspect){ + if(xcd>x1d) { + x1d=xcd-length/2; + x2d=xcd+length/2; + } + else{ + x1d=xcd+length/2; + x2d=xcd-length/2; + } + } + } + } else { + if(aspect){ + if(y1d>y2d) y2d=y1d-length; + else y2d=y1d+length; + } + x2d = x1d; + if(center){ + x1d=x2d=xcd; + if(aspect){ + if(ycd>y1d) { + y1d=ycd-length/2; + y2d=ycd+length/2; + } + else{ + y1d=ycd+length/2; + y2d=ycd-length/2; + } + } + } + } + } + } + if (!magnificationForSubPixel()) { + x1d = Math.round(x1d); y1d = Math.round(y1d); + x2d = Math.round(x2d); y2d = Math.round(y2d); + } + updateCoordinates(x1d, y1d, x2d, y2d); + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; + oldY = y; + oldWidth = width; + oldHeight = height; + } + + protected void mouseDownInHandle(int handle, int sx, int sy) { + super.mouseDownInHandle(handle, sx, sy); //sets state, activeHandle, previousSX&Y + if (getStrokeWidth()<=3) + ic.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR)); + } + + /** Sets the x1d, y1d, x2d, y2d line end points, + * the (legacy) integer coordinates of the end points x1, y1, x2, y2 + * the 'bounds' subpixel rectangle of the Roi superclass (spanned by the end points), + * the int x, y, width, height integer bounds of the superclass (these enclose + * the 'draw' area for 1 pixel width), and + * the coordinates x1R, y1R, x2R, y2R relative to the base x, y of the 'bounds' */ + void updateCoordinates(double x1d, double y1d, double x2d, double y2d) { + this.x1d = x1d; this.y1d = y1d; + this.x2d = x2d; this.y2d = y2d; + Rectangle2D.Double bounds = this.bounds; //local variable (this.bounds may become null asynchronously upon nudge) + if (bounds == null) bounds = new Rectangle2D.Double(); + bounds.x = Math.min(x1d, x2d); + bounds.y = Math.min(y1d, y2d); + bounds.width = Math.abs(x2d - x1d); + bounds.height = Math.abs(y2d - y1d); + setIntBounds(bounds); //sets x, y, width, height + x1R = x1d - bounds.x; y1R = y1d - bounds.y; + x2R = x2d - bounds.x; y2R = y2d - bounds.y; + x1=(int)x1d; y1=(int)y1d; x2=(int)x2d; y2=(int)y2d; + this.bounds = bounds; + } + + /** Draws this line on the image. */ + public void draw(Graphics g) { + Color color = strokeColor!=null? strokeColor:ROIColor; + boolean isActiveOverlayRoi = !overlay && isActiveOverlayRoi(); + mag = getMagnification(); + if (isActiveOverlayRoi) { + if (color==Color.cyan) + color = Color.magenta; + else + color = Color.cyan; + } + g.setColor(color); + x1d=getXBase()+x1R; y1d=getYBase()+y1R; x2d=getXBase()+x2R; y2d=getYBase()+y2R; + x1=(int)x1d; y1=(int)y1d; x2=(int)x2d; y2=(int)y2d; + int sx1 = screenXD(x1d); + int sy1 = screenYD(y1d); + int sx2 = screenXD(x2d); + int sy2 = screenYD(y2d); + int sx3 = sx1 + (sx2-sx1)/2; + int sy3 = sy1 + (sy2-sy1)/2; + Graphics2D g2d = (Graphics2D)g; + setRenderingHint(g2d); + boolean cbar = overlay && mag<1.0 && Math.abs(getStrokeWidth()-CalibrationBar.STROKE_WIDTH)<0.0001; + if (stroke!=null && !isActiveOverlayRoi && !cbar) + g2d.setStroke(getScaledStroke()); + else if (cbar) + g2d.setStroke(onePixelWide); + if (wideLine && !isActiveOverlayRoi && !cbar) { + double dx = sx2 - sx1; + double dy = sy2 - sy1; + double len = length(dx, dy); + dx *= 0.5*mag/len; //half-pixel extension, corresponding to getFloatPolygon or convertLineToArea + dy *= 0.5*mag/len; + g2d.draw(new Line2D.Double(sx1-dx, sy1-dy, sx2+dx, sy2+dy)); + } else + g.drawLine(sx1, sy1, sx2, sy2); + if (wideLine && !overlay) { + g2d.setStroke(onePixelWide); + g.setColor(getColor()); + g.drawLine(sx1, sy1, sx2, sy2); + } + if (!overlay) { + handleColor = strokeColor!=null?strokeColor:ROIColor; + drawHandle(g, sx1, sy1); + handleColor=Color.white; + drawHandle(g, sx2, sy2); + drawHandle(g, sx3, sy3); + } + if (state!=NORMAL) + showStatus(); + if (updateFullWindow) + {updateFullWindow = false; imp.draw();} + } + + public void showStatus() { + IJ.showStatus(imp.getLocationAsString((int)Math.round(x2d),(int)Math.round(y2d))+ + ", angle=" + IJ.d2s(getAngle()) + ", length=" + IJ.d2s(getLength())); + } + + public double getAngle() { + return getFloatAngle(x1d, y1d, x2d, y2d); + } + + /** Returns the length of this line. */ + public double getLength() { + if (imp==null || IJ.altKeyDown()) + return getRawLength(); + else { + Calibration cal = imp.getCalibration(); + return Math.sqrt(sqr((x2d-x1d)*cal.pixelWidth) + sqr((y2d-y1d)*cal.pixelHeight)); + } + } + + /** Returns the length of this line in pixels. */ + public double getRawLength() { + return Math.sqrt(sqr(x2d-x1d)+sqr(y2d-y1d)); + } + + /** Returns the pixel values along this line. + * The line roi must have an associated ImagePlus */ + public double[] getPixels() { + double[] profile; + if (getStrokeWidth()<=1) { + ImageProcessor ip = imp.getProcessor(); + profile = ip.getLine(x1d, y1d, x2d, y2d); + } else { + ImageProcessor ip2 = (new Straightener()).rotateLine(imp,(int)getStrokeWidth()); + if (ip2==null) return new double[0]; + int width = ip2.getWidth(); + int height = ip2.getHeight(); + if (ip2 instanceof FloatProcessor) + return ProfilePlot.getColumnAverageProfile(new Rectangle(0,0,width,height),ip2); + profile = new double[width]; + double[] aLine; + ip2.setInterpolate(false); + for (int y=0; y1) { + if ((x==x1&&y==y1) || (x==x2&&y==y2)) + return true; + else + return getPolygon().contains(x,y); + } else + return false; + } + + protected void handleMouseDown(int sx, int sy) { + super.handleMouseDown(sx, sy); + startxd = ic.offScreenXD(sx); + startyd = ic.offScreenYD(sy); + } + + /** Returns a handle number if the specified screen coordinates are + inside or near a handle, otherwise returns -1. */ + public int isHandle(int sx, int sy) { + int size = HANDLE_SIZE+5; + if (getStrokeWidth()>1) size += (int)Math.log(getStrokeWidth()); + int halfSize = size/2; + int sx1 = screenXD(getXBase()+x1R) - halfSize; + int sy1 = screenYD(getYBase()+y1R) - halfSize; + int sx2 = screenXD(getXBase()+x2R) - halfSize; + int sy2 = screenYD(getYBase()+y2R) - halfSize; + int sx3 = sx1 + (sx2-sx1)/2-1; + int sy3 = sy1 + (sy2-sy1)/2-1; + if (sx>=sx1&&sx<=sx1+size&&sy>=sy1&&sy<=sy1+size) return 0; + if (sx>=sx2&&sx<=sx2+size&&sy>=sy2&&sy<=sy2+size) return 1; + if (sx>=sx3&&sx<=sx3+size+2&&sy>=sy3&&sy<=sy3+size+2) return 2; + return -1; + } + + public static int getWidth() { + return lineWidth; + } + + public static void setWidth(int w) { + if (w<1) w = 1; + int max = 500; + if (w>max) { + ImagePlus imp2 = WindowManager.getCurrentImage(); + if (imp2!=null) { + max = Math.max(max, imp2.getWidth()); + max = Math.max(max, imp2.getHeight()); + } + if (w>max) w = max; + } + lineWidth = w; + widthChanged = true; + } + + public void setStrokeWidth(float width) { + super.setStrokeWidth(width); + if (getStrokeColor()==Roi.getColor()) + wideLine = true; + } + + protected int clipRectMargin() { + return 4; + } + + /** Nudge end point of line by one pixel. */ + public void nudgeCorner(int key) { + if (ic==null) return; + double inc = 1.0/ic.getMagnification(); + switch(key) { + case KeyEvent.VK_UP: y2R-=inc; break; + case KeyEvent.VK_DOWN: y2R+=inc; break; + case KeyEvent.VK_LEFT: x2R-=inc; break; + case KeyEvent.VK_RIGHT: x2R+=inc; break; + } + grow(screenXD(x+x2R), screenYD(y+y2R)); + notifyListeners(RoiListener.MOVED); + showStatus(); + } + + /** Always returns true. */ + public boolean subPixelResolution() { + return true; + } + + public void setLocation(int x, int y) { + setLocation((double)x, (double)y); + } + + /** Sets the x coordinate of the leftmost and y coordinate of the topmost end point */ + public void setLocation(double x, double y) { + updateCoordinates(x+x1R, y+y1R, x+x2R, y+y2R); + } + + public FloatPolygon getRotationCenter() { + double xcenter = x1d + (x2d-x1d)/2.0; + double ycenter = y1d + (y2d-y1d)/2.0; + FloatPolygon p = new FloatPolygon(); + p.addPoint(xcenter,ycenter); + return p; + } + + /** + * Dedicated point iterator for thin lines. + * The iterator is based on (an improved version of) the algorithm used by + * the original method {@code ImageProcessor.getLine(double, double, double, double)}. + * Improvements are (a) that the endpoint is drawn too and (b) every line + * point is visited only once, duplicates are skipped. + * + * Author: Wilhelm Burger (04/2017) + */ + public static class PointIterator implements Iterator { + private double x1, y1; + private final int n; + private final double xinc, yinc; + private double x, y; + private int u, v; + private int u_prev, v_prev; + private int i; + + public PointIterator(Line line) { + this(line.x1d, line.y1d, line.x2d, line.y2d); + } + + public PointIterator(double x1, double y1, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + double dx = x2 - x1; + double dy = y2 - y1; + this.n = (int) Math.ceil(Math.sqrt(dx * dx + dy * dy)); + this.xinc = dx / n; + this.yinc = dy / n; + x = x1; + y = y1; + u = (int) Math.round(x - 0.5); + v = (int) Math.round(y - 0.5); + u_prev = Integer.MIN_VALUE; + v_prev = Integer.MIN_VALUE; + i = 0; + } + + @Override + public boolean hasNext() { + return i <= n; // needs to be '<=' to include last segment (point)! + } + + @Override + public Point next() { + if (i > n) throw new NoSuchElementException(); + Point p = new Point(u, v); // the current (next) point + moveToNext(); + return p; + } + + // move to next point by skipping duplicate points + private void moveToNext() { + do { + i = i + 1; + x = x1 + i * xinc; + y = y1 + i * yinc; + u_prev = u; + v_prev = v; + u = (int) Math.round(x - 0.5); + v = (int) Math.round(y - 0.5); + } while (i <= n && u == u_prev && v == v_prev); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public Iterator iterator() { + if (getStrokeWidth() <= 1.0) + return new PointIterator(this); // use the specific thin-line iterator + else + return super.iterator(); // fall back on Roi's iterator + } + +} diff --git a/src/ij/gui/MessageDialog.java b/src/ij/gui/MessageDialog.java new file mode 100644 index 0000000..7d8b942 --- /dev/null +++ b/src/ij/gui/MessageDialog.java @@ -0,0 +1,85 @@ +package ij.gui; +import ij.*; +import java.awt.*; +import java.awt.event.*; + +/** A modal dialog box that displays information. Based on the + InfoDialogclass from "Java in a Nutshell" by David Flanagan. */ +public class MessageDialog extends Dialog implements ActionListener, KeyListener, WindowListener { + protected Button button; + protected MultiLineLabel label; + private boolean escapePressed; + + public MessageDialog(Frame parent, String title, String message) { + super(parent, title, true); + setLayout(new BorderLayout()); + if (message==null) message = ""; + Font font = null; + double scale = Prefs.getGuiScale(); + if (scale>1.0) { + font = getFont(); + if (font!=null) + font = font.deriveFont((float)(font.getSize()*scale)); + else + font = new Font("SansSerif", Font.PLAIN, (int)(12*scale)); + setFont(font); + } + label = new MultiLineLabel(message); + if (font!=null) + label.setFont(font); + else if (!IJ.isLinux()) + label.setFont(new Font("SansSerif", Font.PLAIN, 14)); + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.CENTER, 15, 15)); + panel.add(label); + add("Center", panel); + button = new Button(" OK "); + button.addActionListener(this); + button.addKeyListener(this); + panel = new Panel(); + panel.setLayout(new FlowLayout()); + panel.add(button); + add("South", panel); + if (ij.IJ.isMacintosh()) + setResizable(false); + pack(); + GUI.centerOnImageJScreen(this); + addWindowListener(this); + show(); + } + + public void actionPerformed(ActionEvent e) { + dispose(); + } + + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyDown(keyCode); + escapePressed = keyCode==KeyEvent.VK_ESCAPE; + if (keyCode==KeyEvent.VK_ENTER || escapePressed) + dispose(); + } + + public void keyReleased(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyUp(keyCode); + } + + public void keyTyped(KeyEvent e) {} + + public void windowClosing(WindowEvent e) { + dispose(); + } + + public boolean escapePressed() { + return escapePressed; + } + + public void windowActivated(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + +} diff --git a/src/ij/gui/MultiLineLabel.java b/src/ij/gui/MultiLineLabel.java new file mode 100644 index 0000000..054b3ad --- /dev/null +++ b/src/ij/gui/MultiLineLabel.java @@ -0,0 +1,108 @@ +package ij.gui; +import java.awt.*; +import java.util.*; + +/**Custom component for displaying multiple lines. Based on + MultiLineLabel class from "Java in a Nutshell" by David Flanagan.*/ +public class MultiLineLabel extends Canvas { + String[] lines; + int num_lines; + int margin_width = 6; + int margin_height = 6; + int line_height; + int line_ascent; + int[] line_widths; + int min_width, max_width; + + // Breaks the specified label up into an array of lines. + public MultiLineLabel(String label) { + init(label); + } + + public MultiLineLabel(String label, int minimumWidth) { + init(label); + min_width = minimumWidth; + } + + private void init(String text) { + StringTokenizer t = new StringTokenizer(text, "\n"); + num_lines = t.countTokens(); + lines = new String[num_lines]; + line_widths = new int[num_lines]; + for (int i=0; i max_width) max_width = line_widths[i]; + } + } + + + public void setText(String text) { + init(text); + measure(); + repaint(); + } + + public void setFont(Font f) { + super.setFont(f); + measure(); + repaint(); + } + + + // This method is invoked after our Canvas is first created + // but before it can actually be displayed. After we've + // invoked our superclass's addNotify() method, we have font + // metrics and can successfully call measure() to figure out + // how big the label is. + public void addNotify() { + super.addNotify(); + measure(); + } + + + // Called by a layout manager when it wants to + // know how big we'd like to be. + public Dimension getPreferredSize() { + return new Dimension(Math.max(min_width, max_width + 2*margin_width), + num_lines * line_height + 2*margin_height); + } + + + // Called when the layout manager wants to know + // the bare minimum amount of space we need to get by. + public Dimension getMinimumSize() { + return new Dimension(Math.max(min_width, max_width), num_lines * line_height); + } + + // Draws the label + public void paint(Graphics g) { + int x, y; + Dimension d = this.getSize(); + if (!ij.IJ.isLinux()) setAntialiasedText(g); + y = line_ascent + (d.height - num_lines * line_height)/2; + for(int i = 0; i < num_lines; i++, y += line_height) { + x = margin_width; + g.drawString(lines[i], x, y); + } + } + + void setAntialiasedText(Graphics g) { + Graphics2D g2d = (Graphics2D)g; + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + } + +} diff --git a/src/ij/gui/NewImage.java b/src/ij/gui/NewImage.java new file mode 100644 index 0000000..e14e15c --- /dev/null +++ b/src/ij/gui/NewImage.java @@ -0,0 +1,466 @@ +package ij.gui; + +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.awt.event.*; +import java.util.*; +import ij.*; +import ij.process.*; + +/** New image dialog box plus several static utility methods for creating images.*/ +public class NewImage { + + public static final int GRAY8=0, GRAY16=1, GRAY32=2, RGB=3; + public static final int FILL_BLACK=1, FILL_RAMP=2, FILL_NOISE=3, FILL_RANDOM=3, + FILL_WHITE=4, CHECK_AVAILABLE_MEMORY=8, SIGNED_INT=16; + private static final int OLD_FILL_WHITE=0; + + static final String TYPE = "new.type"; + static final String FILL = "new.fill"; + static final String WIDTH = "new.width"; + static final String HEIGHT = "new.height"; + static final String SLICES = "new.slices"; + + private static String name = "Untitled"; + private static int staticWidth = Prefs.getInt(WIDTH, 512); + private static int staticHeight = Prefs.getInt(HEIGHT, 512); + private static int staticSlices = Prefs.getInt(SLICES, 1); + private static int staticType = Prefs.getInt(TYPE, GRAY8); + private static int staticFillWith = Prefs.getInt(FILL, FILL_BLACK); + private static String[] types = {"8-bit", "16-bit", "32-bit", "RGB"}; + private static String[] fill = {"White", "Black", "Ramp", "Noise"}; + private int gwidth, gheight, gslices, gtype, gfill; + + public NewImage() { + openImage(); + } + + static boolean createStack(ImagePlus imp, ImageProcessor ip, int nSlices, int type, int options) { + int fill = getFill(options); + int width = imp.getWidth(); + int height = imp.getHeight(); + long bytesPerPixel = 1; + if (type==GRAY16) bytesPerPixel = 2; + else if (type==GRAY32||type==RGB) bytesPerPixel = 4; + long size = (long)width*height*nSlices*bytesPerPixel; + int sizeThreshold = fill==FILL_NOISE?10:250; + boolean bigStack = size/(1024*1024)>=sizeThreshold; + String size2 = size/(1024*1024)+"MB ("+width+"x"+height+"x"+nSlices+")"; + if ((options&CHECK_AVAILABLE_MEMORY)!=0) { + long max = IJ.maxMemory(); // - 100*1024*1024; + if (max>0) { + long inUse = IJ.currentMemory(); + long available = max - inUse; + if (size>available) + System.gc(); + inUse = IJ.currentMemory(); + available = max-inUse; + if (size>available) { + IJ.error("Insufficient Memory", "There is not enough free memory to allocate a \n" + + size2+" stack.\n \n" + + "Memory available: "+available/(1024*1024)+"MB\n" + + "Memory in use: "+IJ.freeMemory()+"\n \n" + + "More information can be found in the \"Memory\"\n" + + "sections of the ImageJ installation notes at\n" + + "\""+IJ.URL+"/docs/install/\"."); + return false; + } + } + } + ImageStack stack = imp.createEmptyStack(); + boolean signedInt = (options&SIGNED_INT)!=0; + if (type==RGB && signedInt) + stack.setOptions("32-bit int"); + int inc = nSlices/40; + if (inc<1) inc = 1; + if (bigStack) + IJ.showStatus("Allocating "+size2+". Press 'Esc' to abort."); + IJ.resetEscape(); + try { + stack.addSlice(null, ip); + for (int i=2; i<=nSlices; i++) { + if ((i%inc)==0 && bigStack) + IJ.showProgress(i, nSlices); + Object pixels2 = null; + switch (type) { + case GRAY8: pixels2 = new byte[width*height]; + if (fill==FILL_NOISE) + fillNoiseByte(new ByteProcessor(width,height,(byte[])pixels2)); + break; + case GRAY16: pixels2 = new short[width*height]; + if (fill==FILL_NOISE) + fillNoiseShort(new ShortProcessor(width,height,(short[])pixels2,null)); + break; + case GRAY32: pixels2 = new float[width*height]; + if (fill==FILL_NOISE) + fillNoiseFloat(new FloatProcessor(width,height,(float[])pixels2,null)); + break; + case RGB: pixels2 = new int[width*height]; + if (fill==FILL_NOISE) { + if (signedInt) + fillNoiseInt(new IntProcessor(width,height,(int[])pixels2)); + else + fillNoiseRGB(new ColorProcessor(width,height,(int[])pixels2), false); + } + break; + } + if (signedInt && (fill==FILL_WHITE||fill==FILL_RAMP) || ((type==RGB)&&(fill!=FILL_NOISE))) + System.arraycopy(ip.getPixels(), 0, pixels2, 0, width*height); + stack.addSlice(null, pixels2); + if (IJ.escapePressed()) {IJ.beep(); break;}; + } + } + catch(OutOfMemoryError e) { + IJ.outOfMemory(imp.getTitle()); + stack.trim(); + } + IJ.showStatus(""); + if (bigStack) + IJ.showProgress(nSlices, nSlices); + if (stack.size()>1) + imp.setStack(null, stack); + return true; + } + + static int getFill(int options) { + int fill = options&7; + if (fill==OLD_FILL_WHITE) + fill = FILL_WHITE; + if (fill==7||fill==6||fill==5) + fill = FILL_BLACK; + return fill; + } + + public static ImagePlus createByteImage(String title, int width, int height, int slices, int options) { + int fill = getFill(options); + int size = getSize(width, height); + if (size<0) return null; + byte[] pixels = new byte[size]; + ImageProcessor ip = new ByteProcessor(width, height, pixels, null); + switch (fill) { + case FILL_WHITE: + for (int i=0; i1) { + boolean ok = createStack(imp, ip, slices, GRAY8, options); + if (!ok) imp = null; + } + return imp; + } + + private static void fillNoiseByte(ImageProcessor ip) { + ip.add(127); + ip.noise(31); + } + + public static ImagePlus createRGBImage(String title, int width, int height, int slices, int options) { + int fill = getFill(options); + int size = getSize(width, height); + if (size<0) return null; + int[] pixels = new int[size]; + ColorProcessor ip = new ColorProcessor(width, height, pixels); + switch (fill) { + case FILL_WHITE: + for (int i=0; i1) { + boolean ok = createStack(imp, ip, slices, RGB, options); + if (!ok) imp = null; + } + return imp; + } + + public static ImagePlus createIntImage(String title, int width, int height, int slices, int options) { + int fill = getFill(options); + int size = getSize(width, height); + if (size<0) return null; + int[] pixels = new int[size]; + IntProcessor ip = new IntProcessor(width, height, pixels); + switch (fill) { + case FILL_RAMP: + int[] ramp = new int[width]; + double inc = ((double)Integer.MAX_VALUE - (double)Integer.MIN_VALUE)/width; + for (int i=0; i1) { + boolean ok = createStack(imp, ip, slices, RGB, options); + if (!ok) imp = null; + } + return imp; + } + + private static void fillNoiseRGB(ColorProcessor ip, boolean sp) { + int width = ip.getWidth(); + int height = ip.getHeight(); + ByteProcessor rr = new ByteProcessor(width, height); + ByteProcessor gg = new ByteProcessor(width, height); + ByteProcessor bb = new ByteProcessor(width, height); + if (sp) IJ.showProgress(0.0); + rr.add(127); if (sp) IJ.showProgress(0.05); + gg.add(127); if (sp) IJ.showProgress(0.10); + bb.add(127); if (sp) IJ.showProgress(0.15); + rr.noise(31); if (sp) IJ.showProgress(0.40); + gg.noise(31); if (sp) IJ.showProgress(0.65); + bb.noise(31); if (sp) IJ.showProgress(0.90); + if (sp) IJ.showProgress(1.0); + ip.setChannel(1,rr); ip.setChannel(2,gg); ip.setChannel(3,bb); + } + + private static void fillNoiseInt(ImageProcessor ip) { + Random rnd = new Random(); + int n = ip.getPixelCount(); + double std =((double)Integer.MAX_VALUE - (double)Integer.MIN_VALUE)*0.12; + for (int i=0; i1) { + boolean ok = createStack(imp, ip, slices, GRAY16, options); + if (!ok) imp = null; + } + imp.getProcessor().setMinAndMax(0, 65535); // default display range + return imp; + } + + private static void fillNoiseShort(ImageProcessor ip) { + ip.add(32767); + ip.noise(7940); + } + + /** + * @deprecated + * Short images are always unsigned. + */ + public static ImagePlus createUnsignedShortImage(String title, int width, int height, int slices, int options) { + return createShortImage(title, width, height, slices, options); + } + + public static ImagePlus createFloatImage(String title, int width, int height, int slices, int options) { + int fill = getFill(options); + int size = getSize(width, height); + if (size<0) return null; + float[] pixels = new float[size]; + ImageProcessor ip = new FloatProcessor(width, height, pixels, null); + switch (fill) { + case FILL_WHITE: case FILL_BLACK: + break; + case FILL_RAMP: + float[] ramp = new float[width]; + for (int i=0; i1) { + boolean ok = createStack(imp, ip, slices, GRAY32, options); + if (!ok) imp = null; + } + if (fill!=FILL_NOISE) + imp.getProcessor().setMinAndMax(0.0, 1.0); // default display range + return imp; + } + + private static void fillNoiseFloat(ImageProcessor ip) { + ip.noise(1); + } + + private static int getSize(int width, int height) { + long size = (long)width*height; + if (size>Integer.MAX_VALUE) { + IJ.error("Image is too large. ImageJ does not support\nsingle images larger than 2 gigapixels."); + return -1; + } else + return (int)size; + } + + public static void open(String title, int width, int height, int nSlices, int type, int options) { + int bitDepth = 8; + if (type==GRAY16) bitDepth = 16; + else if (type==GRAY32) bitDepth = 32; + else if (type==RGB) bitDepth = 24; + long startTime = System.currentTimeMillis(); + ImagePlus imp = createImage(title, width, height, nSlices, bitDepth, options); + if (imp!=null) { + WindowManager.checkForDuplicateName = true; + imp.show(); + IJ.showStatus(IJ.d2s(((System.currentTimeMillis()-startTime)/1000.0),2)+" seconds"); + } + } + + public static ImagePlus createImage(String title, int width, int height, int nSlices, int bitDepth, int options) { + ImagePlus imp = null; + switch (bitDepth) { + case 8: imp = createByteImage(title, width, height, nSlices, options); break; + case 16: imp = createShortImage(title, width, height, nSlices, options); break; + case 32: imp = createFloatImage(title, width, height, nSlices, options); break; + case 24: + if ((options&SIGNED_INT)!=0) + imp = createIntImage(title, width, height, nSlices, options); + else + imp = createRGBImage(title, width, height, nSlices, options); + break; + default: throw new IllegalArgumentException("Invalid bitDepth: "+bitDepth); + } + return imp; + } + + boolean showDialog() { + if (staticTypeRGB) + staticType = GRAY8; + if (staticFillWithFILL_NOISE) + staticFillWith = FILL_WHITE; + GenericDialog gd = new GenericDialog("New Image..."); + gd.addStringField("Name:", name, 12); + gd.addChoice("Type:", types, types[staticType]); + gd.addChoice("Fill with:", fill, fill[staticFillWith]); + gd.addNumericField("Width:", staticWidth, 0, 5, "pixels"); + gd.addNumericField("Height:", staticHeight, 0, 5, "pixels"); + gd.addNumericField("Slices:", staticSlices, 0, 5, ""); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + name = gd.getNextString(); + String s = gd.getNextChoice(); + if (s.startsWith("8")) + gtype = GRAY8; + else if (s.startsWith("16")) + gtype = GRAY16; + else if (s.endsWith("RGB") || s.endsWith("rgb")) + gtype = RGB; + else + gtype = GRAY32; + gfill = gd.getNextChoiceIndex(); + gwidth = (int)gd.getNextNumber(); + gheight = (int)gd.getNextNumber(); + gslices = (int)gd.getNextNumber(); + if (gslices<1) gslices = 1; + if (gwidth<1 || gheight<1) { + IJ.error("New Image", "Width and height must be >0"); + return false; + } else { + if (!IJ.isMacro()) { + staticWidth = gwidth; + staticHeight = gheight; + staticSlices = gslices; + staticType = gtype; + staticFillWith = gfill; + } + return true; + } + } + + void openImage() { + if (!showDialog()) + return; + try { + open(name, gwidth, gheight, gslices, gtype, gfill); + } catch(OutOfMemoryError e) { + IJ.outOfMemory("New Image..."); + } + } + + /** Called when ImageJ quits. */ + public static void savePreferences(Properties prefs) { + prefs.put(TYPE, Integer.toString(staticType)); + prefs.put(FILL, Integer.toString(staticFillWith)); + prefs.put(WIDTH, Integer.toString(staticWidth)); + prefs.put(HEIGHT, Integer.toString(staticHeight)); + prefs.put(SLICES, Integer.toString(staticSlices)); + } + +} diff --git a/src/ij/gui/NonBlockingGenericDialog.java b/src/ij/gui/NonBlockingGenericDialog.java new file mode 100644 index 0000000..55dc6b3 --- /dev/null +++ b/src/ij/gui/NonBlockingGenericDialog.java @@ -0,0 +1,102 @@ +package ij.gui; +import ij.*; +import java.awt.event.*; +import java.awt.EventQueue; +import java.awt.GraphicsEnvironment; +import java.awt.Frame; + +/** This is an extension of GenericDialog that is non-modal. + * @author Johannes Schindelin + */ +public class NonBlockingGenericDialog extends GenericDialog { + ImagePlus imp; //when non-null, this dialog gets closed when the image is closed + WindowListener windowListener; //checking for whether the associated window gets closed + + public NonBlockingGenericDialog(String title) { + super(title, null); + setModal(false); + IJ.protectStatusBar(false); + instance = this; + } + + public synchronized void showDialog() { + super.showDialog(); + if (isMacro()) + return; + if (!IJ.macroRunning()) { // add to Window menu on event dispatch thread + final NonBlockingGenericDialog thisDialog = this; + EventQueue.invokeLater(new Runnable() { + public void run() { + WindowManager.addWindow(thisDialog); + } + }); + } + if (imp != null) { + ImageWindow win = imp.getWindow(); + if (win != null) { //when the associated image closes, also close the dialog + final NonBlockingGenericDialog gd = this; + windowListener = new WindowAdapter() { + public void windowClosed(WindowEvent e) { + cancelDialogAndClose(); + } + }; + win.addWindowListener(windowListener); + } + } + try { + wait(); + } catch (InterruptedException e) { } + } + + /** Gets called if the associated image window is closed */ + private void cancelDialogAndClose() { + super.windowClosing(null); // sets wasCanceled=true and does dispose() + } + + public synchronized void actionPerformed(ActionEvent e) { + super.actionPerformed(e); + if (!isVisible()) + notify(); + } + + public synchronized void keyPressed(KeyEvent e) { + super.keyPressed(e); + if (wasOKed() || wasCanceled()) + notify(); + } + + public synchronized void windowClosing(WindowEvent e) { + super.windowClosing(e); + if (wasOKed() || wasCanceled()) + notify(); + } + + public void dispose() { + super.dispose(); + WindowManager.removeWindow(this); + if (imp != null) { + ImageWindow win = imp.getWindow(); + if (win != null && windowListener != null) + win.removeWindowListener(windowListener); + } + } + + /** Obsolete, replaced by GUI.newNonBlockingDialog(String,ImagePlus). */ + public static GenericDialog newDialog(String title, ImagePlus imp) { + return GUI.newNonBlockingDialog(title, imp); + } + + /** Obsolete, replaced by GUI.newNonBlockingDialog(String). */ + public static GenericDialog newDialog(String title) { + return GUI.newNonBlockingDialog(title); + } + + /** Put the dialog into the foreground when the image we work on gets into the foreground */ + @Override + public void windowActivated(WindowEvent e) { + if ((e.getWindow() instanceof ImageWindow) && e.getOppositeWindow()!=this) + toFront(); + WindowManager.setWindow(this); + } + +} diff --git a/src/ij/gui/OvalRoi.java b/src/ij/gui/OvalRoi.java new file mode 100644 index 0000000..7923313 --- /dev/null +++ b/src/ij/gui/OvalRoi.java @@ -0,0 +1,430 @@ +package ij.gui; +import java.awt.*; +import java.awt.image.*; +import java.awt.geom.*; +import ij.*; +import ij.process.*; +import ij.measure.Calibration; + +/** Oval region of interest */ +public class OvalRoi extends Roi { + + /** Creates an OvalRoi.*/ + public OvalRoi(int x, int y, int width, int height) { + super(x, y, width, height); + type = OVAL; + } + + /** Creates an OvalRoi using double arguments.*/ + public OvalRoi(double x, double y, double width, double height) { + super(x, y, width, height); + type = OVAL; + } + + /** Creates an OvalRoi. */ + public static OvalRoi create(double x, double y, double width, double height) { + return new OvalRoi(x, y, width, height); + } + + /** Starts the process of creating a user-defined OvalRoi. */ + public OvalRoi(int x, int y, ImagePlus imp) { + super(x, y, imp); + type = OVAL; + } + + /** @deprecated */ + public OvalRoi(int x, int y, int width, int height, ImagePlus imp) { + this(x, y, width, height); + setImage(imp); + } + + /** Feret (caliper width) values, see ij.gui.Roi.getFeretValues(). + * The superclass method of calculating this via the convex hull is less accurate for the MinFeret + * because it does not get the exact minor axis. */ + public double[] getFeretValues() { + double[] a = new double[FERET_ARRAYSIZE]; + double pw=1.0, ph=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } + boolean highAspect = ph*height > pw*width; + a[0] = highAspect ? height*ph : width*pw; // (max)Feret + a[1] = highAspect ? 90.0 : 0.0; // (max)Feret angle + a[2] = highAspect ? width*pw : height*ph; // MinFeret + a[3] = (x + (highAspect ? 0.5*width : 0)) * pw; //FeretX scaled + a[4] = (y + (highAspect ? height : 0.5*height)) * ph;//FeretY scaled + int i = FERET_ARRAY_POINTOFFSET; + a[i++] = x + (highAspect ? 0.5*width : 0); //MaxFeret start + a[i++] = y + (highAspect ? height : 0.5*height); + a[i++] = x + (highAspect ? 0.5*width : width); //MaxFeret end + a[i++] = y + (highAspect ? 0 : 0.5*height); + a[i++] = x + (highAspect ? 0 : 0.5*width); //MinFeret start + a[i++] = y + (highAspect ? 0.5*height : height); + a[i++] = x + (highAspect ? width : 0.5*width); //MinFeret end + a[i++] = y + (highAspect ? 0.5*height : 0); + return a; + } + + protected void moveHandle(int sx, int sy) { + double asp; + if (clipboard!=null) return; + int ox = offScreenX(sx); + int oy = offScreenY(sy); + //IJ.log("moveHandle: "+activeHandle+" "+ox+" "+oy); + int x1=x, y1=y, x2=x+width, y2=y+height, xc=x+width/2, yc=y+height/2; + int w2 = (int)(0.14645*width); + int h2 = (int)(0.14645*height); + if (width > 7 && height > 7) { + asp = (double)width/(double)height; + asp_bk = asp; + } else + asp = asp_bk; + switch (activeHandle) { + case 0: x=ox-w2; y=oy-h2; break; + case 1: y=oy; break; + case 2: x2=ox+w2; y=oy-h2; break; + case 3: x2=ox; break; + case 4: x2=ox+w2; y2=oy+h2; break; + case 5: y2=oy; break; + case 6: x=ox-w2; y2=oy+h2; break; + case 7: x=ox; break; + } + //if (x<0) x=0; if (y<0) y=0; + if (x=x2) { + width=1; + x=x2=xc; + } + if (y>=y2) { + height=1; + y=y2=yc; + } + + } + + if (constrain) { + if (activeHandle==1 || activeHandle==5) width=height; + else height=width; + + if (x>=x2) { + width=1; + x=x2=xc; + } + if (y>=y2) { + height=1; + y=y2=yc; + } + switch(activeHandle){ + case 0: + x=x2-width; + y=y2-height; + break; + case 1: + x=xc-width/2; + y=y2-height; + break; + case 2: + y=y2-height; + break; + case 3: + y=yc-height/2; + break; + case 5: + x=xc-width/2; + break; + case 6: + x=x2-width; + break; + case 7: + y=yc-height/2; + x=x2-width; + break; + } + if (center){ + x=xc-width/2; + y=yc-height/2; + } + } + + if (aspect && !constrain) { + if (activeHandle==1 || activeHandle==5) width=(int)Math.rint((double)height*asp); + else height=(int)Math.rint((double)width/asp); + + switch (activeHandle) { + case 0: + x=x2-width; + y=y2-height; + break; + case 1: + x=xc-width/2; + y=y2-height; + break; + case 2: + y=y2-height; + break; + case 3: + y=yc-height/2; + break; + case 5: + x=xc-width/2; + break; + case 6: + x=x2-width; + break; + case 7: + y=yc-height/2; + x=x2-width; + break; + } + if (center) { + x=xc-width/2; + y=yc-height/2; + } + // Attempt to preserve aspect ratio when roi very small: + if (width<8) { + if (width<1) width = 1; + height=(int)Math.rint((double)width/asp_bk); + } + if (height<8) { + if (height<1) height =1; + width=(int)Math.rint((double)height*asp_bk); + } + } + + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX=x; oldY=y; + oldWidth=width; oldHeight=height; + cachedMask = null; + bounds = null; + } + + public void draw(Graphics g) { + Color color = strokeColor!=null? strokeColor:ROIColor; + if (fillColor!=null) color = fillColor; + g.setColor(color); + mag = getMagnification(); + int sw = (int)(width*mag); + int sh = (int)(height*mag); + int sx1 = screenX(x); + int sy1 = screenY(y); + if (subPixelResolution() && bounds!=null) { + sw = (int)(bounds.width*mag); + sh = (int)(bounds.height*mag); + sx1 = screenXD(bounds.x); + sy1 = screenYD(bounds.y); + } + int sw2 = (int)(0.14645*width*mag); + int sh2 = (int)(0.14645*height*mag); + int sx2 = sx1+sw/2; + int sy2 = sy1+sh/2; + int sx3 = sx1+sw; + int sy3 = sy1+sh; + Graphics2D g2d = (Graphics2D)g; + if (stroke!=null) + g2d.setStroke(getScaledStroke()); + setRenderingHint(g2d); + if (fillColor!=null) { + if (!overlay && isActiveOverlayRoi()) { + g.setColor(Color.cyan); + g.drawOval(sx1, sy1, sw, sh); + } else + g.fillOval(sx1, sy1, sw, sh); + } else + g.drawOval(sx1, sy1, sw, sh); + if (clipboard==null && !overlay) { + drawHandle(g, sx1+sw2, sy1+sh2); + drawHandle(g, sx3-sw2, sy1+sh2); + drawHandle(g, sx3-sw2, sy3-sh2); + drawHandle(g, sx1+sw2, sy3-sh2); + drawHandle(g, sx2, sy1); + drawHandle(g, sx3, sy2); + drawHandle(g, sx2, sy3); + drawHandle(g, sx1, sy2); + } + drawPreviousRoi(g); + if (updateFullWindow) + {updateFullWindow = false; imp.draw();} + if (state!=NORMAL) showStatus(); + } + + /** Draws an outline of this OvalRoi on the image. */ + public void drawPixels(ImageProcessor ip) { + Polygon p = getPolygon(); + if (p.npoints>0) { + int saveWidth = ip.getLineWidth(); + if (getStrokeWidth()>1f) + ip.setLineWidth((int)Math.round(getStrokeWidth())); + ip.drawPolygon(p); + ip.setLineWidth(saveWidth); + } + if (Line.getWidth()>1 || getStrokeWidth()>1) + updateFullWindow = true; + } + + /** Returns this OvalRoi as a Polygon that outlines the mask, in image pixel coordinates. */ + public Polygon getPolygon() { + return getPolygon(true); + } + + /** Returns this OvalRoi as a Polygon that outlines the mask. + * @param absoluteCoordinates determines whether to use image pixel coordinates + * instead of coordinates relative to roi origin. */ + Polygon getPolygon(boolean absoluteCoordinates) { + ImageProcessor mask = getMask(); + Wand wand = new Wand(mask); + wand.autoOutline(width/2,height/2, 255, 255); + if (absoluteCoordinates) + for (int i=0; i=sx1+sw2&&sx<=sx1+sw2+size&&sy>=sy1+sh2&&sy<=sy1+sh2+size) return 0; + if (sx>=sx2&&sx<=sx2+size&&sy>=sy1&&sy<=sy1+size) return 1; + if (sx>=sx3-sw2&&sx<=sx3-sw2+size&&sy>=sy1+sh2&&sy<=sy1+sh2+size) return 2; + if (sx>=sx3&&sx<=sx3+size&&sy>=sy2&&sy<=sy2+size) return 3; + if (sx>=sx3-sw2&&sx<=sx3-sw2+size&&sy>=sy3-sh2&&sy<=sy3-sh2+size) return 4; + if (sx>=sx2&&sx<=sx2+size&&sy>=sy3&&sy<=sy3+size) return 5; + if (sx>=sx1+sw2&&sx<=sx1+sw2+size&&sy>=sy3-sh2&&sy<=sy3-sh2+size) return 6; + if (sx>=sx1&&sx<=sx1+size&&sy>=sy2&&sy<=sy2+size) return 7; + return -1; + } + + public ImageProcessor getMask() { + ImageProcessor mask = cachedMask; + if (mask!=null && mask.getPixels()!=null && mask.getWidth()==width && mask.getHeight()==height) + return mask; + mask = new ByteProcessor(width, height); + double a=width/2.0, b=height/2.0; + double a2=a*a, b2=b*b; + a -= 0.5; b -= 0.5; + double xx, yy; + int offset; + byte[] pixels = (byte[])mask.getPixels(); + for (int y=0; y { + private Vector list; + private boolean label; + private boolean drawNames; + private boolean drawBackgrounds; + private Color labelColor; + private Font labelFont; + private boolean scalableLabels; + private boolean isCalibrationBar; + private boolean selectable = true; + private boolean draggable = true; + + /** Constructs an empty Overlay. */ + public Overlay() { + list = new Vector(); + } + + /** Constructs an Overlay and adds the specified ROI. */ + public Overlay(Roi roi) { + list = new Vector(); + if (roi!=null) + list.add(roi); + } + + /** Adds an ROI to this Overlay. */ + public void add(Roi roi) { + if (roi!=null) + list.add(roi); + } + + /** Adds an ROI to this Overlay using the specified name. */ + public void add(Roi roi, String name) { + roi.setName(name); + add(roi); + } + + /** Adds an ROI to this Overlay. */ + public void addElement(Roi roi) { + if (roi!=null) + list.add(roi); + } + + /** Replaces the ROI at the specified index. */ + public void set(Roi roi, int index) { + if (index<0 || index>=list.size()) + throw new IllegalArgumentException("set: index out of range"); + if (roi!=null) + list.set(index, roi); + } + + /** Removes the ROI with the specified index from this Overlay. */ + public void remove(int index) { + if (index>=0) + list.remove(index); + } + + /** Removes the specified ROI from this Overlay. */ + public void remove(Roi roi) { + list.remove(roi); + } + + /** Removes all ROIs that have the specified name. */ + public void remove(String name) { + if (name==null) return; + for (int i=size()-1; i>=0; i--) { + if (name.equals(get(i).getName())) + remove(i); + } + } + + /** Removes all the ROIs in this Overlay. */ + public void clear() { + list.clear(); + } + + /** Returns the ROI with the specified index or null if the index is invalid. */ + public Roi get(int index) { + try { + return (Roi)list.get(index); + } catch(Exception e) { + return null; + } + } + + /** Returns the ROI with the specified name or null if not found. */ + public Roi get(String name) { + int index = getIndex(name); + if (index==-1) + return null; + else + return get(index); + } + + /** Returns the index of the ROI with the specified name, or -1 if not found. */ + public int getIndex(String name) { + if (name==null) return -1; + Roi[] rois = toArray(); + for (int i=rois.length-1; i>=0; i--) { + if (name.equals(rois[i].getName())) + return i; + } + return -1; + } + + /** Returns the index of the last ROI that contains the point (x,y) + or null if no ROI contains the point. */ + public int indexAt(int x, int y) { + Roi[] rois = toArray(); + for (int i=rois.length-1; i>=0; i--) { + if (contains(rois[i],x,y)) + return i; + } + return -1; + } + + private boolean contains(Roi roi, int x, int y) { + if (roi==null) return false; + if (roi instanceof Line) + return (((Line)roi).getFloatPolygon(10)).contains(x,y); + else + return roi.contains(x,y); + } + + /** Returns 'true' if this Overlay contains the specified ROI. */ + public boolean contains(Roi roi) { + return list.contains(roi); + } + + /** Returns the number of ROIs in this Overlay. */ + public int size() { + return list.size(); + } + + /** Returns on array containing the ROIs in this Overlay. */ + public Roi[] toArray() { + Roi[] array = new Roi[list.size()]; + return (Roi[])list.toArray(array); + } + + /** Returns on array containing the ROIs with the specified indexes. */ + public Roi[] toArray(int[] indexes) { + ArrayList rois = new ArrayList(); + for (int i=0; i=0 && indexes[i]0?bounds.x:0; + int dy = bounds.y>0?bounds.y:0; + if (dx>0 || dy>0) + overlay2.translate(-dx, -dy); + return overlay2; + } + + /** Removes ROIs having positions outside of the + * interval defined by firstSlice and lastSlice. + * Marcel Boeglin, September 2013 + */ + public void crop(int firstSlice, int lastSlice) { + for (int i=size()-1; i>=0; i--) { + Roi roi = get(i); + int position = roi.getPosition(); + if (position>0) { + if (positionlastSlice) + remove(i); + else + roi.setPosition(position-firstSlice+1); + } + } + } + + /** Removes ROIs having a C, Z or T coordinate outside the volume + * defined by firstC, lastC, firstZ, lastZ, firstT and lastT. + * Marcel Boeglin, September 2013 + */ + public void crop(int firstC, int lastC, int firstZ, int lastZ, int firstT, int lastT) { + int nc = lastC-firstC+1, nz = lastZ-firstZ+1, nt = lastT-firstT+1; + boolean toCStack = nz==1 && nt==1; + boolean toZStack = nt==1 && nc==1; + boolean toTStack = nc==1 && nz==1; + Roi roi; + int c, z, t, c2, z2, t2; + for (int i=size()-1; i>=0; i--) { + roi = get(i); + c = roi.getCPosition(); + z = roi.getZPosition(); + t = roi.getTPosition(); + c2 = c-firstC+1; + z2 = z-firstZ+1; + t2 = t-firstT+1; + if (toCStack) + roi.setPosition(c2); + else if (toZStack) + roi.setPosition(z2); + else if (toTStack) + roi.setPosition(t2); + else + roi.setPosition(c2, z2, t2); + if ((c2<1||c2>nc) && c>0 || (z2<1||z2>nz) && z>0 || (t2<1||t2>nt) && t>0) + remove(i); + } + } + + /** Returns the bounds of this overlay. */ + /* + public Rectangle getBounds() { + if (size()==0) + return new Rectangle(0,0,0,0); + int xmin = Integer.MAX_VALUE; + int xmax = -Integer.MAX_VALUE; + int ymin = Integer.MAX_VALUE; + int ymax = -Integer.MAX_VALUE; + Roi[] rois = toArray(); + for (int i=0; ixmax) xmax = r.x+r.width; + if (r.y+r.height>ymax) ymax = r.y+r.height; + } + return new Rectangle(xmin, ymin, xmax-xmin, ymax-ymin); + } + */ + + /* Returns the Roi that results from XORing all the ROIs + * in this overlay that have an index in the array ‘indexes’. + */ + public Roi xor(int[] indexes) { + return Roi.xor(toArray(indexes)); + } + + /** Returns a new Overlay that has the same properties as this one. */ + public Overlay create() { + Overlay overlay2 = new Overlay(); + overlay2.drawLabels(label); + overlay2.drawNames(drawNames); + overlay2.drawBackgrounds(drawBackgrounds); + overlay2.setLabelColor(labelColor); + overlay2.setLabelFont(labelFont, scalableLabels); + overlay2.setIsCalibrationBar(isCalibrationBar); + overlay2.selectable(selectable); + overlay2.setDraggable(draggable); + return overlay2; + } + + /** Returns a clone of this Overlay. */ + public Overlay duplicate() { + Roi[] rois = toArray(); + Overlay overlay2 = create(); + for (int i=0; i v) {list = v;} + + Vector getVector() {return list;} + + /** Set 'false' to prevent ROIs in this overlay from being activated + by clicking on their labels or by a long clicking. */ + public void selectable(boolean selectable) { + this.selectable = selectable; + } + + /** Returns 'true' if ROIs in this overlay can be activated + by clicking on their labels or by a long press. */ + public boolean isSelectable() { + return selectable; + } + + /** Set 'false' to prevent ROIs in this overlay from being dragged by their labels. */ + public void setDraggable(boolean draggable) { + this.draggable = draggable; + } + + /** Returns 'true' if ROIs in this overlay can be dragged by their labels. */ + public boolean isDraggable() { + return draggable; + } + + public boolean scalableLabels() { + return scalableLabels; + } + + public String toString() { + return "Overlay[size="+size()+" "+(scalableLabels?"scale":"")+" "+Colors.colorToString(getLabelColor())+"]"; + } + + /** Updates overlays created by the particle analyzer + after rows are deleted from the Results table. */ + public static void updateTableOverlay(ImagePlus imp, int first, int last, int tableSize) { + if (imp==null) + return; + Overlay overlay = imp.getOverlay(); + if (overlay==null) + return; + if (overlay.size()!=tableSize) + return; + if (first<0) + first = 0; + if (last>tableSize-1) + last = tableSize-1; + if (first>last) + return; + String name1 = overlay.get(0).getName(); + String name2 = overlay.get(overlay.size()-1).getName(); + if (!"1".equals(name1) || !(""+tableSize).equals(name2)) + return; + int count = last-first+1; + if (overlay.size()==count && !IJ.isMacro()) { + if (count==1 || IJ.showMessageWithCancel("ImageJ", "Delete "+overlay.size()+" element overlay? ")) + imp.setOverlay(null); + return; + } + for (int i=0; i iterator() { + final Overlay overlay = this; + + Iterator it = new Iterator() { + private int index = -1; + + /** Returns 'true' if next element exists. */ + @Override + public boolean hasNext() { + if (index+12 + private static final int LEGEND_PADDING = 4; //pixels around legend text etc + private static final int LEGEND_LINELENGTH = 20; //length of lines in legend + private static final int USUALLY_ENLARGE = 1, ALWAYS_ENLARGE = 2; //enlargeRange settings + private static final double RELATIVE_ARROWHEAD_SIZE = 0.2; //arrow heads have 1/5 of vector length + private static final int MIN_ARROWHEAD_LENGTH = 3; + private static final int MAX_ARROWHEAD_LENGTH = 20; + private static final String MULTIPLY_SYMBOL = "\u00B7"; //middot, default multiplication symbol for scientific notation. Use setOptions("msymbol=\\u00d7") for '×' + + PlotProperties pp = new PlotProperties(); //size, range, formatting etc, for easy serialization + PlotProperties ppSnapshot; //copy for reverting + Vector allPlotObjects = new Vector(); //all curves, labels etc., also serialized for saving/reading + Vector allPlotObjectsSnapshot; //copy for reverting + private PlotVirtualStack stack; + /** For high-resolution plots, everything will be scaled with this number. Otherwise, must be 1.0. + * (creating margins, saving PlotProperties etc only supports scale=1.0) */ + float scale = 1.0f; + Rectangle frame = null; //the clip frame, do not use for image scale + //The following are the margin sizes actually used. They are modified for font size and also scaled for high-resolution plots + int leftMargin = LEFT_MARGIN, rightMargin = RIGHT_MARGIN, topMargin = TOP_MARGIN, bottomMargin = BOTTOM_MARGIN; + int frameWidth; //width corresponding to plot range; frame.width is larger by 1 + int frameHeight; //height corresponding to plot range; frame.height is larger by 1 + int preferredPlotWidth = PlotWindow.plotWidth; //default size of plot frame (not taking 'High-Resolution' scale factor into account) + int preferredPlotHeight = PlotWindow.plotHeight; + + double xMin = Double.NaN, xMax, yMin, yMax; //current plot range, logarithm if log axis + double[] currentMinMax = new double[]{Double.NaN, 0, Double.NaN, 0}; //current plot range, xMin, xMax, yMin, yMax (values, not logarithm if log axis) + double[] defaultMinMax = new double[]{Double.NaN, 0, Double.NaN, 0}; //default plot range + double[] savedMinMax = new double[]{Double.NaN, 0, Double.NaN, 0}; //keeps previous range for revert + int[] enlargeRange; // whether to enlarge the range slightly to avoid values at the border (0=off, USUALLY_ENLARGE, ALWAYS_ENLARGE) + boolean logXAxis, logYAxis; // whether to really use log axis (never for small relative range) + //for passing on what should be kept when 'live' plotting (PlotMaker), but note that 'COPY_EXTRA_OBJECTS' is also on for live plotting: + int templateFlags = COPY_SIZE | COPY_LABELS | COPY_AXIS_STYLE | COPY_CONTENTS_STYLE | COPY_LEGEND; + private int dsize = PlotWindow.getDefaultFontSize(); + Font defaultFont = FontUtil.getFont("Arial",Font.PLAIN,dsize); //default font for labels, axis, etc. + Font currentFont = defaultFont; // font as changed by setFont or setFontSize, must never be null + private double xScale, yScale; // pixels per data unit + private int xBasePxl, yBasePxl; // pixel coordinates corresponding to 0 + private int maxIntervals = 12; // maximum number of intervals between ticks or grid lines + private int tickLength = 7; // length of major ticks + private int minorTickLength = 3; // length of minor ticks + private Color gridColor = new Color(0xc0c0c0); // light gray + private ImageProcessor ip; + private ImagePlus imp; // if we have an ImagePlus, updateAndDraw on changes + private String title; + private boolean invertedLut; // grayscale plots only, set in Edit>Options>Appearance + private boolean plotDrawn; + PlotMaker plotMaker; // for PlotMaker interface, handled by PlotWindow + private Color currentColor; // for next objects added + private Color currentColor2; // 2nd color for next object added (e.g. line for CONNECTED_CIRCLES) + float currentLineWidth; + private int currentJustification = LEFT; + private boolean ignoreForce2Grid; // after explicit setting of range (limits), ignore 'FORCE2GRID' flags + //private boolean snapToMinorGrid; // snap to grid when zooming to selection + private static double SEPARATED_BAR_WIDTH=0.5; // for plots with separate bars (e.g. categories), fraction of space, 0.1-1.0 + double[] steps; // x & y interval between numbers, major ticks & grid lines, remembered for redrawing the grid + private int objectToReplace = -1; // index in allPlotObjects, for replace + private Point2D.Double textLoc; // remembers position of previous addLabel call (replaces text if at the same position) + private int textIndex; // remembers index of previous addLabel call (for replacing if at the same position) + + /** Constructs a new Plot with the default options. + * Use add(shape,xvalues,yvalues) to add curves. + * @param title the window title + * @param xLabel the x-axis label; see setXYLabels for seting categories on an axis via the label + * @param yLabel the y-axis label; see setXYLabels for seting categories on an axis via the label + * @see #add(String,double[],double[]) + * @see #add(String,double[]) + */ + public Plot(String title, String xLabel, String yLabel) { + this(title, xLabel, yLabel, (float[])null, (float[])null, getDefaultFlags()); + } + + /** Obsolete, replaced by "new Plot(title,xLabel,yLabel); add(shape,x,y);". + * @deprecated + */ + public Plot(String title, String xLabel, String yLabel, float[] x, float[] y) { + this(title, xLabel, yLabel, x, y, getDefaultFlags()); + } + + /** Obsolete, replaced by "new Plot(title,xLabel,yLabel); add(shape,x,y);". + * @deprecated + */ + public Plot(String title, String xLabel, String yLabel, double[] x, double[] y) { + this(title, xLabel, yLabel, x!=null?Tools.toFloat(x):null, y!=null?Tools.toFloat(y):null, getDefaultFlags()); + } + + /** This version of the constructor has a 'flags' argument for + controlling whether ticks, grid, etc. are present and whether + the axes are logarithmic */ + public Plot(String title, String xLabel, String yLabel, int flags) { + this(title, xLabel, yLabel, (float[])null, (float[])null, flags); + } + + /** Obsolete, replaced by "new Plot(title,xLabel,yLabel,flags); add(shape,x,y);". + * @deprecated + */ + public Plot(String title, String xLabel, String yLabel, float[] xValues, float[] yValues, int flags) { + this.title = title; + pp.axisFlags = flags; + setXYLabels(xLabel, yLabel); + if (yValues != null && yValues.length>0) { + addPoints(xValues, yValues, /*yErrorBars=*/null, LINE, /*label=*/null); + allPlotObjects.get(0).flags = PlotObject.CONSTRUCTOR_DATA; + } + } + + /** Obsolete, replaced by "new Plot(title,xLabel,yLabel,flags); add(shape,x,y);". + * @deprecated + */ + public Plot(String title, String xLabel, String yLabel, double[] x, double[] y, int flags) { + this(title, xLabel, yLabel, x!=null?Tools.toFloat(x):null, y!=null?Tools.toFloat(y):null, flags); + } + + /** Constructs a new plot from an InputStream and closes the stream. If the ImagePlus is + * non-null, its title and ImageProcessor are used, but the image displayed is not modified. + */ + public Plot(ImagePlus imp, InputStream is) throws IOException, ClassNotFoundException { + ObjectInputStream in = new ObjectInputStream(is); + pp = (PlotProperties)in.readObject(); + allPlotObjects = (Vector)in.readObject(); + in.close(); + if (pp.xLabel.type==8) { + pp.xLabel.updateType(); //convert old (pre-1.52i) type codes for the PlotObjects + pp.yLabel.updateType(); + pp.frame.updateType(); + if (pp.legend != null) pp.legend.updateType(); + for (PlotObject plotObject : allPlotObjects) + plotObject.updateType(); + } + + defaultMinMax = pp.rangeMinMax; + currentFont = nonNullFont(pp.frame.getFont(), currentFont); // best guess in case we want to add a legend + getProcessor(); //prepares scale, calibration etc, but does not plot it yet + this.title = imp != null ? imp.getTitle() : "Untitled Plot"; + if (imp != null) { + this.imp = imp; + ip = imp.getProcessor(); + imp.setIgnoreGlobalCalibration(true); + adjustCalibration(imp.getCalibration()); + imp.setProperty(PROPERTY_KEY, this); + } + } + + /** Obsolete, replaced by "new Plot(title,xLabel,yLabel); add(shape,x,y);". + * @deprecated + */ + public Plot(String dummy, String title, String xLabel, String yLabel, float[] x, float[] y) { + this(title, xLabel, yLabel, x, y, getDefaultFlags()); + } + + /** Writes this plot into an OutputStream containing (1) the serialized PlotProperties and + * (2) the serialized Vector of all 'added' PlotObjects. The stream is NOT closed. + * The plot should have been drawn already. + */ + // Conversion to Streams can be also used to clone plots (not a shallow clone), but this is rather slow. + // Sample code: + // try { + // final PipedOutputStream pos = new PipedOutputStream(); + // final PipedInputStream pis = new PipedInputStream(pos); + // new Thread(new Runnable() { + // final public void run() { + // try { + // Plot p = new Plot(null, pis); + // pis.close(); + // pos.close(); + // p.show(); + // } catch(Exception e) {IJ.handleException(e);}; + // } + // }, "threadMakingPlotFromStream").start(); + // toStream(pos); + // } catch(Exception e) {IJ.handleException(e);} + void toStream(OutputStream os) throws IOException { + //prepare + for (PlotObject plotObject : pp.getAllPlotObjects()) //make sure all fonts are set properly + if (plotObject != null) + plotObject.setFont(nonNullFont(plotObject.getFont(), currentFont)); + pp.rangeMinMax = currentMinMax; + //write + ObjectOutputStream out = new ObjectOutputStream(os); + out.writeObject(pp); + out.writeObject(allPlotObjects); + } + + /** Writes this plot into a byte array containing (1) the serialized PlotProperties and + * (2) the serialized Vector of all 'added' PlotObjects. + * The plot should have been drawn already. Returns null on error (which should never happen). */ + public byte[] toByteArray() { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + toStream(bos); + bos.close(); + return bos.toByteArray(); + } catch (Exception e) { + IJ.handleException(e); + return null; + } + } + + /** Returns the title of the image showing the plot (if any) or title of the plot */ + public String getTitle() { + return imp == null ? title : imp.getTitle(); + } + + /** Sets the x-axis and y-axis range. Saves the new limits as default (so the 'R' field sets the limits to these). + * Updates the image if existing. + * Accepts NaN values to indicate auto-range. */ + public void setLimits(double xMin, double xMax, double yMin, double yMax) { + setLimitsNoUpdate(xMin, xMax, yMin, yMax); + makeLimitsDefault(); + ignoreForce2Grid = true; + if (plotDrawn) + setLimitsToDefaults(true); + } + + /** Sets the x-axis and y-axis range. Accepts NaN values to indicate auto range. + * Does not update the image and leaves the default limits (for reset via the 'R' field) untouched. */ + void setLimitsNoUpdate(double xMin, double xMax, double yMin, double yMax) { + boolean containsNaN = (Double.isNaN(xMin + xMax + yMin + yMax)); + if (containsNaN && getNumPlotObjects(PlotObject.XY_DATA|PlotObject.ARROWS, false)==0)//can't apply auto-range without data + return; + double[] range = {xMin, xMax, yMin, yMax}; + if (containsNaN) { //auto range for at least one limit + double[] extrema = getMinAndMax(true, ALL_AXES_RANGE); + boolean[] auto = new boolean[range.length]; + for (int i = 0; i < range.length; i++) + if (Double.isNaN(range[i])) { + auto[i] = true; + range[i] = extrema[i]; + } + for (int a = 0; aOptions>Plots. + * @see #setFrameSize(int,int) + * @see #setWindowSize(int,int) + */ + public void setSize(int width, int height) { + if (ip != null && width == ip.getWidth() && height == ip.getHeight()) + return; + Dimension minSize = getMinimumSize(); + pp.width = Math.max(width, minSize.width); + pp.height = Math.max(height, minSize.height); + scale = 1.0f; + ip = null; + if (plotDrawn) updateImage(); + } + + /** The size of the plot including borders with axis labels etc., in pixels */ + public Dimension getSize() { + if (ip == null) + getBlankProcessor(); + return new Dimension(ip.getWidth(), ip.getHeight()); + } + + /** Sets the plot frame size in (unscaled) pixels. This size does not include the + * borders with the axis labels. Also sets the scale to 1.0. + * This frame size in pixels divided by the data range defines the image scale. + * This method does not check for the minimum size MIN_FRAMEWIDTH, MIN_FRAMEHEIGHT. + * Note that the black frame will have an outer size that is one pixel larger + * (when plotted with a linewidth of one pixel). + * @see #setWindowSize(int,int) + */ + public void setFrameSize(int width, int height) { + if (pp.width <= 0) { //plot not drawn yet? Just remember as preferred size + preferredPlotWidth = width; + preferredPlotHeight = height; + scale = 1.0f; + } else { + makeMarginValues(); + width += leftMargin+rightMargin; + height += topMargin+bottomMargin; + setSize(width, height); + } + } + + /** Sets the plot window size in pixels. + * @see #setFrameSize(int,int) + */ + public void setWindowSize(int width, int height) { + scale = 1.0f; + makeMarginValues(); + int titleBarHeight = 22; + int infoHeight = 11; + double scale = Prefs.getGuiScale(); + if (scale>1.0) + infoHeight = (int)(infoHeight*scale); + int buttonPanelHeight = 45; + if (pp.width <= 0) { //plot not drawn yet? + int extraWidth = leftMargin+rightMargin+ImageWindow.HGAP*2; + int extraHeight = topMargin+bottomMargin+titleBarHeight+infoHeight+buttonPanelHeight; + if (extraWidthplot may be null; then the call has no effect. */ + public void useTemplate(Plot plot) { + useTemplate(plot, templateFlags); + } + + /** Adjusts the format (style) with another plot as a template. Flags determine what to + * copy from the template; these can be X_RANGE, Y_RANGE, COPY_SIZE, COPY_LABELS, COPY_AXIS_STYLE, + * COPY_CONTENTS_STYLE (hidden items are ignored), and COPY_LEGEND. + * plot may be null; then the call has no effect. */ + public void useTemplate(Plot plot, int templateFlags) { + if (plot == null) return; + this.defaultFont = plot.defaultFont; + this.currentFont = plot.currentFont; + this.currentLineWidth = plot.currentLineWidth; + this.currentColor = plot.currentColor; + if ((templateFlags & COPY_AXIS_STYLE) != 0) { + this.pp.axisFlags = plot.pp.axisFlags; + this.pp.frame = plot.pp.frame.deepClone(); + } + if ((templateFlags & COPY_LABELS) != 0) { + this.pp.xLabel.label = plot.pp.xLabel.label; + this.pp.yLabel.label = plot.pp.yLabel.label; + this.pp.xLabel.setFont(plot.pp.xLabel.getFont()); + this.pp.yLabel.setFont(plot.pp.yLabel.getFont()); + } + for (int i=0; i>(i/2)&0x1) != 0) { + currentMinMax[i] = plot.currentMinMax[i]; + if (!plotDrawn) defaultMinMax[i] = plot.currentMinMax[i]; + } + if ((templateFlags & COPY_LEGEND) != 0 && plot.pp.legend != null) + this.pp.legend = plot.pp.legend.deepClone(); + if ((templateFlags & (COPY_LEGEND | COPY_CONTENTS_STYLE)) != 0) { + int plotPObjectIndex = 0; //points to PlotObjects of the templatePlot + int plotPObjectsSize = plot.allPlotObjects.size(); + for (PlotObject plotObject : allPlotObjects) { + if (plotObject.type == PlotObject.XY_DATA && !plotObject.hasFlag(PlotObject.HIDDEN)) { + while(plotPObjectIndex=plotPObjectsSize) break; + if ((templateFlags & COPY_LEGEND) != 0) + plotObject.label = plot.allPlotObjects.get(plotPObjectIndex).label; + if ((templateFlags & COPY_CONTENTS_STYLE) != 0) + setPlotObjectStyle(plotObject, getPlotObjectStyle(plot.allPlotObjects.get(plotPObjectIndex))); + plotPObjectIndex++; + } + } + } + if ((templateFlags & COPY_SIZE) != 0) + setSize(plot.pp.width, plot.pp.height); + + if ((templateFlags & COPY_EXTRA_OBJECTS) != 0) + for (int p = allPlotObjects.size(); p < plot.allPlotObjects.size(); p++) + allPlotObjects.add(plot.allPlotObjects.get(p)); + this.templateFlags = templateFlags; + } + + /** Sets the scale. Everything, including labels, line thicknesses, etc will be scaled by this factor. + * Also multiplies the plot size by this value. Used for 'Create high-resolution plot'. + * Should be called before creating the plot. + * Note that plots with a scale different from 1.0 must not be shown in a PlotWindow, but only as + * simple image in a normal ImageWindow. */ + public void setScale(float scale) { + this.scale = scale; + if (scale > 20f) scale = 20f; + if (scale < 0.7f) scale = 0.7f; + pp.width = sc(pp.width); + pp.height = sc(pp.height); + plotDrawn = false; + } + + /** Sets the labels of the x and y axes. 'xLabel', 'yLabel' may be null. + * If a label has the form {txt1,txt2,txt3}, the corresponding axis will be labeled + * not by numbers but rather with the texts "txt1", "txt2" ... instead of 0, 1, ... + * In this special case, there will be no label for the axis on the plot. + * Call update() thereafter to make the change visible (if it is shown already). */ + public void setXYLabels(String xLabel, String yLabel) { + pp.xLabel.label = xLabel!=null ? xLabel : ""; + pp.yLabel.label = yLabel!=null ? yLabel : ""; + } + + /** Sets the maximum number of intervals in a plot. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setMaxIntervals(int intervals) { + maxIntervals = intervals; + } + + /** Sets the length of the major tick in pixels. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setTickLength(int tickLength) { + tickLength = tickLength; + } + + /** Sets the length of the minor tick in pixels. */ + public void setMinorTickLength(int minorTickLength) { + minorTickLength = minorTickLength; + } + + /** Sets the flags that control the axes format. + * Does not modify the flags for logarithmic axes on/off and the FORCE2GRID flags. + * Call update() thereafter to make the change visible (if it is shown already). */ + public void setFormatFlags(int flags) { + int unchangedFlags = X_LOG_NUMBERS | Y_LOG_NUMBERS | X_FORCE2GRID | Y_FORCE2GRID; + flags = flags & (~unchangedFlags); //remove flags that should not be affected + pp.axisFlags = (pp.axisFlags & unchangedFlags) | flags; + } + + /** Returns the flags that control the axes */ + public int getFlags() { + return pp.axisFlags; + } + + /** Sets the X Axis format to Log or Linear. + * Call update() thereafter to make the change visible (if it is shown already). */ + public void setAxisXLog(boolean axisXLog) { + pp.axisFlags = axisXLog ? pp.axisFlags | X_LOG_NUMBERS : pp.axisFlags & (~X_LOG_NUMBERS); + } + + /** Sets the Y Axis format to Log or Linear. + * Call update() thereafter to make the change visible (if it is shown already). */ + public void setAxisYLog(boolean axisYLog) { + pp.axisFlags = axisYLog ? pp.axisFlags | Y_LOG_NUMBERS : pp.axisFlags & (~Y_LOG_NUMBERS); + } + + /** Sets whether to show major ticks at the x axis. + * Call update() thereafter to make the change visible (if the image is shown already). */ + + public void setXTicks(boolean xTicks) { + pp.axisFlags = xTicks ? pp.axisFlags | X_TICKS : pp.axisFlags & (~X_TICKS); + } + + /** Sets whether to show major ticks at the y axis. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setYTicks(boolean yTicks) { + pp.axisFlags = yTicks ? pp.axisFlags | Y_TICKS : pp.axisFlags & (~Y_TICKS); + } + + /** Sets whether to show minor ticks on the x axis (if linear). Also sets major ticks if true and no grid is set. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setXMinorTicks(boolean xMinorTicks) { + pp.axisFlags = xMinorTicks ? pp.axisFlags | X_MINOR_TICKS : pp.axisFlags & (~X_MINOR_TICKS); + if (xMinorTicks && !hasFlag(X_GRID)) + pp.axisFlags |= X_TICKS; + } + + /** Sets whether to show minor ticks on the y axis (if linear). Also sets major ticks if true and no grid is set. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setYMinorTicks(boolean yMinorTicks) { + pp.axisFlags = yMinorTicks ? pp.axisFlags | Y_MINOR_TICKS : pp.axisFlags & (~Y_MINOR_TICKS); + if (yMinorTicks && !hasFlag(Y_GRID)) + pp.axisFlags |= Y_TICKS; + } + + /** Sets the properties of the axes. Call update() thereafter to make the change visible + * (if the image is shown already). */ + public void setAxes(boolean xLog, boolean yLog, boolean xTicks, boolean yTicks, boolean xMinorTicks, boolean yMinorTicks, + int tickLenght, int minorTickLenght) { + setAxisXLog (xLog); + setAxisYLog (yLog); + setXMinorTicks (xMinorTicks); + setYMinorTicks (yMinorTicks); + setXTicks (xTicks); + setYTicks (yTicks); + setTickLength (tickLenght); + setMinorTickLength(minorTickLenght); + } + + /** Sets log scale in x. Call update() thereafter to make the change visible + * (if the image is shown already). */ + + public void setLogScaleX() { + setAxisXLog(true); + } + + public void setLogScaleY() { + setAxisYLog(true); + } + + /** The default flags, taking PlotWindow.noGridLines, PlotWindow.noTicks into account */ + public static int getDefaultFlags() { + int defaultFlags = 0; + if (!PlotWindow.noGridLines) //note that log ticks are also needed because the range may span less than a decade, then no grid is visible + defaultFlags |= X_GRID | Y_GRID | X_NUMBERS | Y_NUMBERS | X_LOG_TICKS | Y_LOG_TICKS; + if (!PlotWindow.noTicks) + defaultFlags |= X_TICKS | Y_TICKS | X_MINOR_TICKS | Y_MINOR_TICKS | X_NUMBERS | Y_NUMBERS | X_LOG_TICKS | Y_LOG_TICKS; + return defaultFlags; + } + + /** Adds a curve or set of points to this plot, where 'type' is + * "line", "connected circle", "filled", "bar", "separated bar", "circle", "box", "triangle", "diamond", "cross", + * "x" or "dot". Run Help>Examples>JavaScript>Graph Types to see examples. + * If 'type' is in the form "code: ", the macro given is executed to draw the symbol; + * macro variables 'x' and 'y' are the pixel coordinates of the point, 'xval' and 'yval' are the plot data + * and 'i' is the index of the data point (starting with 0 for the first point in the array). + * The drawing including line thickness, font size, etc. be scaled by scale factor 's' (to make high-resolution plots work). + * Example: "code: setFont('sanserif',12*s,'bold anti');drawString(d2s(yval,1),x-14*s,y-4*s);" + * writes the y value for each point above the point. + */ + public void add(String type, double[] xvalues, double[] yvalues) { + int iShape = toShape(type); + addPoints(Tools.toFloat(xvalues), Tools.toFloat(yvalues), null, iShape, iShape==CUSTOM?type.substring(5, type.length()):null); + } + + /** Replaces the specified plot object (curve or set of points). + Equivalent to add() if there are no plot objects. */ + public void replace(int index, String type, double[] xvalues, double[] yvalues) { + if (index>=0 && index0?index:-1; + add(type, xvalues, yvalues); + } + } + + /** Adds a curve, set of points or error bars to this plot, where 'type' is + * "line", "connected circle", "filled", "bar", "separated bar", "circle", "box", + * "triangle", "diamond", "cross", "x", "dot", "error bars" or "xerror bars". + */ + public void add(String type, double[] yvalues) { + int iShape = toShape(type); + if (iShape==-1) + addErrorBars(yvalues); + else if (iShape==-2) + addHorizontalErrorBars(yvalues); + else + addPoints(null, Tools.toFloat(yvalues), null, iShape, iShape==CUSTOM?type.substring(5, type.length()):null); + } + + /** Adds a set of points to the plot or adds a curve if shape is set to LINE. + * @param xValues the x coordinates, or null. If null, integers starting at 0 will be used for x. + * @param yValues the y coordinates (must not be null) + * @param yErrorBars error bars in y, may be null + * @param shape CIRCLE, X, BOX, TRIANGLE, CROSS, DIAMOND, DOT, LINE, CONNECTED_CIRCLES + * @param label Label for this curve or set of points, used for a legend and for listing the plots + */ + public void addPoints(float[] xValues, float[] yValues, float[] yErrorBars, int shape, String label) { + if (xValues==null || xValues.length==0) { + xValues = new float[yValues.length]; + for (int i=0; i=0) + allPlotObjects.set(objectToReplace, new PlotObject(xValues, yValues, yErrorBars, shape, currentLineWidth, currentColor, currentColor2, label)); + else + allPlotObjects.add(new PlotObject(xValues, yValues, yErrorBars, shape, currentLineWidth, currentColor, currentColor2, label)); + objectToReplace = -1; + if (plotDrawn) updateImage(); + } + + /** Adds a set of points to the plot or adds a curve if shape is set to LINE. + * @param x the x coordinates + * @param y the y coordinates + * @param shape CIRCLE, X, BOX, TRIANGLE, CROSS, DIAMOND, DOT, LINE, CONNECTED_CIRCLES + */ + public void addPoints(float[] x, float[] y, int shape) { + addPoints(x, y, null, shape, null); + } + + /** Adds a set of points to the plot using double arrays. */ + public void addPoints(double[] x, double[] y, int shape) { + addPoints(Tools.toFloat(x), Tools.toFloat(y), shape); + } + + /** Returns the number for a given plot symbol shape, -1 for xError and -2 for yError (all case-insensitive) */ + public static int toShape(String str) { + str = str.toLowerCase(Locale.US); + int shape = Plot.CIRCLE; + if (str.contains("curve") || str.contains("line")) + shape = Plot.LINE; + else if (str.contains("connected")) + shape = Plot.CONNECTED_CIRCLES; + else if (str.contains("filled")) + shape = Plot.FILLED; + else if (str.contains("circle")) + shape = Plot.CIRCLE; + else if (str.contains("box")) + shape = Plot.BOX; + else if (str.contains("triangle")) + shape = Plot.TRIANGLE; + else if (str.contains("cross") || str.contains("+")) + shape = Plot.CROSS; + else if (str.contains("diamond")) + shape = Plot.DIAMOND; + else if (str.contains("dot")) + shape = Plot.DOT; + else if (str.contains("xerror")) + shape = -2; + else if (str.contains("error")) + shape = -1; + else if (str.contains("x")) + shape = Plot.X; + else if (str.contains("separate")) + shape = Plot.SEPARATED_BAR; + else if (str.contains("bar")) + shape = Plot.BAR; + if (str.startsWith("code:")) + shape = CUSTOM; + return shape; + } + + /** Adds a set of points to the plot using double ArrayLists. + * Must be called before the plot is displayed. */ + public void addPoints(ArrayList x, ArrayList y, int shape) { + addPoints(getDoubleFromArrayList(x), getDoubleFromArrayList(y), shape); + } + + /** Adds a set of points to the plot or adds a curve if shape is set to LINE. + * @param x the x-coodinates + * @param y the y-coodinates + * @param errorBars half-lengths of the vertical error bars, may be null + * @param shape CIRCLE, X, BOX, TRIANGLE, CROSS, DIAMOND, DOT or LINE + */ + public void addPoints(double[] x, double[] y, double[] errorBars, int shape) { + addPoints(Tools.toFloat(x), Tools.toFloat(y), Tools.toFloat(errorBars), shape, null); + } + + /** Adds a set of points to the plot using double ArrayLists. + * Must be called before the plot is displayed. */ + public void addPoints(ArrayList x, ArrayList y, ArrayList errorBars, int shape) { + addPoints(getDoubleFromArrayList(x), getDoubleFromArrayList(y), getDoubleFromArrayList(errorBars), shape); + } + + public double[] getDoubleFromArrayList(ArrayList list) { + if (list == null) return null; + double[] targ = new double[list.size()]; + for (int i = 0; i < list.size(); i++) + targ[i] = ((Double) list.get(i)).doubleValue(); + return targ; + } + + /** Adds a set of points that will be drawn as ARROWs. + * @param x1 the x-coodinates of the beginning of the arrow + * @param y1 the y-coodinates of the beginning of the arrow + * @param x2 the x-coodinates of the end of the arrow + * @param y2 the y-coodinates of the end of the arrow + */ + public void drawVectors(double[] x1, double[] y1, double[] x2, double[] y2) { + allPlotObjects.add(new PlotObject(Tools.toFloat(x1), Tools.toFloat(y1), + Tools.toFloat(x2), Tools.toFloat(y2), currentLineWidth, currentColor)); + } + + /** + * Adds a set of 'shapes' such as boxes and whiskers + * + * @param shapeType e.g. "boxes width=20" + * @param floatCoords eg[6][3] holding 1 Xval + 5 Yvals for 3 boxes + */ + public void drawShapes(String shapeType, ArrayList floatCoords) { + allPlotObjects.add(new PlotObject(shapeType, floatCoords, currentLineWidth, currentColor, currentColor2)); + } + + public static double calculateDistance(int x1, int y1, int x2, int y2) { + return java.lang.Math.sqrt((x2 - x1)*(double)(x2 - x1) + (y2 - y1)*(double)(y2 - y1)); + } + + /** Adds a set of vectors to the plot using double ArrayLists. + * Does not support logarithmic axes. + * Must be called before the plot is displayed. */ + public void drawVectors(ArrayList x1, ArrayList y1, ArrayList x2, ArrayList y2) { + drawVectors(getDoubleFromArrayList(x1), getDoubleFromArrayList(y1), getDoubleFromArrayList(x2), getDoubleFromArrayList(y2)); + } + + /** Adds vertical error bars to the last data passed to the plot (via the constructor or addPoints). */ + public void addErrorBars(float[] errorBars) { + PlotObject mainObject = getLastCurveObject(); + if (mainObject != null) + mainObject.yEValues = errorBars; + else throw new RuntimeException("Plot can't add y error bars without data"); + } + + /** Adds vertical error bars to the last data passed to the plot (via the constructor or addPoints). */ + public void addErrorBars(double[] errorBars) { + addErrorBars(Tools.toFloat(errorBars)); + } + + /** Adds horizontal error bars to the last data passed to the plot (via the constructor or addPoints). */ + public void addHorizontalErrorBars(float[] xErrorBars) { + PlotObject mainObject = getLastCurveObject(); + if (mainObject != null) + mainObject.xEValues = xErrorBars; + else throw new RuntimeException("Plot can't add x error bars without data"); + } + + /** Adds horizontal error bars to the last data passed to the plot (via the constructor or addPoints). */ + public void addHorizontalErrorBars(double[] xErrorBars) { + addHorizontalErrorBars(Tools.toFloat(xErrorBars)); + } + + /** Draws text at the specified location, where (0,0) + * is the upper left corner of the the plot frame and (1,1) is + * the lower right corner. Uses the justification specified by setJustification(). + * When called with the same position as the previous addLabel call, the text of that previous call is replaced */ + public void addLabel(double x, double y, String label) { + if (textLoc!=null && x==textLoc.getX() && y==textLoc.getY()) + allPlotObjects.set(textIndex, new PlotObject(label, x, y, currentJustification, currentFont, currentColor, PlotObject.NORMALIZED_LABEL)); + else { + allPlotObjects.add(new PlotObject(label, x, y, currentJustification, currentFont, currentColor, PlotObject.NORMALIZED_LABEL)); + textLoc = new Point2D.Double(x,y); + textIndex = allPlotObjects.size()-1; + } + } + + /* Draws text at the specified location, using the coordinate system defined + * by setLimits() and the justification specified by setJustification(). */ + public void addText(String label, double x, double y) { + allPlotObjects.add(new PlotObject(label, x, y, currentJustification, currentFont, currentColor, PlotObject.LABEL)); + } + + /** Adds an automatically positioned legend, where 'labels' can be a tab-delimited or + newline-delimited list of curve or point labels in the sequence these data were added. + Hidden data sets are ignored. + If 'labels' is null or empty, the labels of the data set previously (if any) are used. + To modify the legend's style, call 'setFont' and 'setLineWidth' before 'addLegend'. */ + public void addLegend(String labels) { + addLegend(labels, "auto"); + } + + /** Adds a legend at the position given in 'options', where 'labels' can be tab-delimited or + newline-delimited list of curve or point labels in the sequence these data were added. + Hidden data sets are ignored. + If 'labels' is null or empty, the labels of the data set previously (if any) are used. + To modify the legend's style, call 'setFont' and 'setLineWidth' before 'addLegend'. */ + public void addLegend(String labels, String options) { + int flags = 0; + if (options!=null) { + options = options.toLowerCase(); + if (options.contains("top-left")) + flags |= Plot.TOP_LEFT; + else if (options.contains("top-right")) + flags |= Plot.TOP_RIGHT; + else if (options.contains("bottom-left")) + flags |= Plot.BOTTOM_LEFT; + else if (options.contains("bottom-right")) + flags |= Plot.BOTTOM_RIGHT; + else if (!options.contains("off") && !options.contains("no")) + flags |= Plot.AUTO_POSITION; + if (options.contains("bottom-to-top")) + flags |= Plot.LEGEND_BOTTOM_UP; + if (options.contains("transparent")) + flags |= Plot.LEGEND_TRANSPARENT; + } + setLegend(labels, flags); + } + + /** Adds a legend. The legend will be always drawn last (on top of everything). + * To modify the legend's style, call 'setFont' and 'setLineWidth' before 'addLegend' + * @param labels labels of the points or curves in the sequence of the data were added, tab-delimited or linefeed-delimited. + * The labels of the datasets will be set to these values. If null or not given, the labels set + * previously (if any) will be used. + * Hidden data sets are ignored. + * @param flags Bitwise or of position (AUTO_POSITION, TOP_LEFT etc.), LEGEND_TRANSPARENT, and LEGEND_BOTTOM_UP if desired. + * Updates the image (if it is shown already). */ + public void setLegend(String labels, int flags) { + if (labels != null && labels.length()>0) { + String[] allLabels = labels.split("[\n\t]"); + int iPart = 0; + for (PlotObject plotObject : allPlotObjects) + if (plotObject.type == PlotObject.XY_DATA && !plotObject.hasFlag(PlotObject.HIDDEN)) + if (iPart < allLabels.length) { + String label = allLabels[iPart++]; + if (label!=null && label.length()>0) + plotObject.label = label; + } + } + pp.legend = new PlotObject(currentLineWidth == 0 ? 1 : currentLineWidth, + currentFont, currentColor == null ? Color.black : currentColor, flags); + if (plotDrawn) updateImage(); + } + + /** Sets the label for the plot object nuber 'index' in the sequence they were added. + * With index=-1, sets the label for the last object added. + * For x/y data, the label is used for the legend and as header in getResultsTableWithLabels. + * For Text/Label objects, it affects the label shown (but the plot is not redisplayed). */ + public void setLabel(int index, String label) { + if (index < 0) index = allPlotObjects.size() + index; + allPlotObjects.get(index).label = label; + } + + /** Removes NaNs from the xValues and yValues arrays of all plot objects. */ + public void removeNaNs() { + for (PlotObject plotObj : allPlotObjects){ + if(plotObj != null && plotObj.xValues!= null && plotObj.yValues != null ){ + int oldSize = plotObj.xValues.length; + float[] xVals = new float[oldSize]; + float[] yVals = new float[oldSize]; + int newSize = 0; + for (int kk = 0; kk < oldSize; kk++) { + if (!Float.isNaN(plotObj.xValues[kk] + plotObj.yValues[kk])) { + xVals[newSize] = plotObj.xValues[kk]; + yVals[newSize] = plotObj.yValues[kk]; + newSize++; + } + } + if (newSize < oldSize) { + plotObj.xValues = new float[newSize]; + plotObj.yValues = new float[newSize]; + System.arraycopy(xVals, 0, plotObj.xValues, 0, newSize); + System.arraycopy(yVals, 0, plotObj.yValues, 0, newSize); + } + } + } + } + + /** Returns an array of the available curve types ("Line", "Bar", "Circle", etc). */ + public String[] getTypes() { + return SORTED_SHAPES; + } + + /** Sets the justification used by addLabel(), where justification + * is Plot.LEFT, Plot.CENTER or Plot.RIGHT. Default is LEFT. */ + public void setJustification(int justification) { + currentJustification = justification; + } + + /** Changes the drawing color for the next objects that will be added to the plot. + * For selecting the color of the curve passed with the constructor, + * use setColor before draw. + * The frame and labels are always drawn in black. */ + public void setColor(Color c) { + currentColor = c; + currentColor2 = null; + } + + public void setColor(String color) { + setColor(Colors.decode(color, Color.black)); + } + + /** Changes the drawing color for the next objects that will be added to the plot. + * It also sets secondary color: This is the color of the line for CONNECTED_CIRCLES, + * and the color for filling open symbols (CIRCLE, BOX, TRIANGLE). + * Set it to null or use the one-argument call setColor(color) to disable filling. + * For selecting the color of the curve passed with the constructor, + * use setColor before draw. + * The frame and labels are always drawn in black. */ + public void setColor(Color c, Color c2) { + currentColor = c; + currentColor2 = c2; + } + + /** Sets the drawing colors for the next objects that will be added to the plot. */ + public void setColor(String c1, String c2) { + setColor(Colors.decode(c1, Color.black), Colors.decode(c2, null)); + } + + /** Set the plot frame background color. */ + public void setBackgroundColor(Color c) { + pp.frame.color2 = c; + } + + /** Set the plot frame background color. */ + public void setBackgroundColor(String c) { + setBackgroundColor(Colors.decode(c,Color.white)); + } + + /** Changes the line width for the next objects that will be added to the plot. */ + public void setLineWidth(int lineWidth) { + currentLineWidth = lineWidth > 0 ? lineWidth : 0.01f; + } + + /** Changes the line width for the next objects that will be added to the plot. + * After all objects have been added, set the line width to the width desired + * for the frame around the plot (max. 3) */ + public void setLineWidth(float lineWidth) { + currentLineWidth = lineWidth > 0.01 ? lineWidth : 0.01f; + } + + /** Draws a line using the coordinate system defined by setLimits(). */ + public void drawLine(double x1, double y1, double x2, double y2) { + allPlotObjects.add(new PlotObject(x1, y1, x2, y2, currentLineWidth, 0, currentColor, PlotObject.LINE)); + } + + /** Draws a line using a normalized 0-1, 0-1 coordinate system, + * with (0,0) at the top left and (1,1) at the lower right corner. + * This is the same coordinate system used by addLabel(x,y,label). + */ + public void drawNormalizedLine(double x1, double y1, double x2, double y2) { + allPlotObjects.add(new PlotObject(x1, y1, x2, y2, currentLineWidth, 0, currentColor, PlotObject.NORMALIZED_LINE)); + } + + /** Draws a line using the coordinate system defined by setLimits(). */ + public void drawDottedLine(double x1, double y1, double x2, double y2, int step) { + allPlotObjects.add(new PlotObject(x1, y1, x2, y2, currentLineWidth, step, currentColor, PlotObject.DOTTED_LINE)); + } + + /** Sets the font size for all following addLabel() etc. operations. The currently set font when + * displaying the plot determines the font of all labels & numbers. + * After the plot has been shown, sets the font for the numbers and the legend (if present). + * If the plot is hown already, call update() thereafter to make the change visible. */ + public void setFontSize(int size) { + setFont(-1, (float)size); + } + + /** Sets the font for all following addLabel() etc. operations. The currently set font when + * displaying the plot determines the font of all labels & numbers. + * After the plot has been shown, sets the font for the numbers and the legend (if present); + * use setFont(char, Font) to set these fonts individually. + * If the plot is hown already, call update() thereafter to make the change visible. */ + public void setFont(Font font) { + if (font == null) font = defaultFont; + currentFont = font; + if (plotDrawn) { + pp.frame.setFont(font); + if (pp.legend != null) + pp.legend.setFont(font); + } + } + + /** Sets the font size and style for all following addLabel() etc. operations. This leaves + * the font name and style of the previously used fonts unchanged. The currently set font + * when displaying the plot determines the font of the numbers at the axes. + * That font also sets the default label font size, which may be overridden by + * setAxisLabelFontSize or setXLabelFont, setYLabelFont. + * After the plot has been shown, sets the font for the numbers and the legend (if present); + * use setFont(char, Font) to set these fonts individually. + * Styles are defined in the Font class, e.g. Font.PLAIN, Font.BOLD. + * Set style to -1 to leave the style unchanged. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setFont(int style, float size) { + if (size < 9) size = 9f; + if (size > 36) size = 36f; + Font previousFont = nonNullFont(pp.frame.getFont(), currentFont); + if (style < 0) style = previousFont.getStyle(); + setFont(previousFont.deriveFont(style, size)); + } + + /** Sets the x and y label font size and style. Styles are defined + * in the Font class, e.g. Font.PLAIN, Font.BOLD. + * Set style to -1 to leave the style unchanged. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setAxisLabelFont(int style, float size) { + if (size < 9) size = 9f; + if (size > 33) size = 33f; + pp.xLabel.setFont(nonNullFont(pp.xLabel.getFont(), currentFont)); + pp.yLabel.setFont(nonNullFont(pp.yLabel.getFont(), currentFont)); + setXLabelFont(pp.xLabel.getFont().deriveFont(style < 0 ? pp.xLabel.getFont().getStyle() : style, size)); + setYLabelFont(pp.xLabel.getFont().deriveFont(style < 0 ? pp.yLabel.getFont().getStyle() : style, size)); + } + + /** Sets the xLabelFont; must not be null. If this method is not used, the last setFont + * of setFontSize call before displaying the plot determines the font, or if neither + * was called, the font size of the Plot Options is used. */ + public void setXLabelFont(Font font) { + pp.xLabel.setFont(font); + } + + /** Sets the yLabelFont; must not be null. */ + public void setYLabelFont(Font font) { + pp.yLabel.setFont(font); + } + + /** Determines whether to use antialiased text (default true) */ + public void setAntialiasedText(boolean antialiasedText) { + pp.antialiasedText = antialiasedText; + } + + /** Returns the font currently used (e.g. for the next 'addLabel') */ + public Font getCurrentFont() { + return currentFont != null ? currentFont : defaultFont; + } + + /** Returns the default font for the plot */ + public Font getDefaultFont() { + return defaultFont; + } + + /** Gets the font for xLabel ('x'), yLabel('y'), numbers ('f' for 'frame') or the legend ('l'). + * Returns null if the given PlotObject does not exist or its font is null */ + public Font getFont(char c) { + PlotObject plotObject = pp.getPlotObject(c); + if (plotObject != null) + return plotObject.getFont(); + else + return null; + } + + /** Sets the font for xLabel ('x'), yLabel('y'), numbers ('f' for 'frame') or the legend ('l') */ + public void setFont(char c, Font font) { + PlotObject plotObject = pp.getPlotObject(c); + if (plotObject != null) + plotObject.setFont(font); + } + + /** Gets the label String of the xLabel ('x'), yLabel('y') or the legend ('l'). + * Returns null if the given PlotObject does not exist or its label is null */ + public String getLabel(char c) { + PlotObject plotObject = pp.getPlotObject(c); + if (plotObject != null) + return plotObject.label; + else + return null; + } + + /** Gets the flags of the xLabel ('x'), yLabel('y') or the legend ('l'). + * Returns -1 if the given PlotObject does not exist */ + public int getObjectFlags(char c) { + PlotObject plotObject = pp.getPlotObject(c); + if (plotObject != null) + return plotObject.flags; + else + return -1; + } + + /** Get the x coordinates of the data set passed with the constructor (if not null) + * or otherwise of the data set of the first 'addPoints'. Returns null if neither exists */ + public float[] getXValues() { + PlotObject p = getMainCurveObject(); + return p==null ? null : p.xValues; + } + + /** Get the y coordinates of the data set passed with the constructor (if not null) + * or otherwise of the data set of the first 'addPoints'. Returns null if neither exists */ + public float[] getYValues() { + PlotObject p = getMainCurveObject(); + return p==null ? null : p.yValues; + } + + /** Get the data of the n-th Plot Object containing xy data in the sequence they were added + * (Other Plot Objects such as labels, arrows, lines, shapes and hidden PlotObjects are not counted). + * The array returned has elements [0] x data, [1] y data, [2] x error bars, [3] y error bars. + * If no error bars are given, the corresponding arrays are null. + * Returns null if there is no Plot Object with xy data with this index. + * @see #getDataObjectDesignations() + **/ + public float[][] getDataObjectArrays(int index) { + int i = 0; + for (PlotObject plotObject : allPlotObjects) { + if (plotObject.type != PlotObject.XY_DATA || plotObject.hasFlag(PlotObject.HIDDEN)) continue; + if (index == i) + return new float[][] {plotObject.xValues, plotObject.yValues, plotObject.xEValues, plotObject.yEValues}; + i++; + } + return null; + } + + /** Gets an array with human-readable designations of the PlotObjects (curves, labels, ...) + * in the sequence they were added (the object passed with the constructor is first, + * even though it is plotted last). Hidden PlotObjects are included. **/ + public String[] getPlotObjectDesignations() { + return getPlotObjectDesignations(-1, true); + } + + /** Gets an array with human-readable designations of the PlotObjects containing xy data + * in the sequence they were added. Other Plot Objects such as labels, arrows, lines, + * shapes and hidden PlotObjects are not counted. + * (the object passed with the constructor is first, even though it is plotted last). */ + public String[] getDataObjectDesignations() { + return getPlotObjectDesignations(PlotObject.XY_DATA, false); + } + + /** Returns the number of PlotObjects (curves, labels, ...) passed with the constructor or added by 'add' or 'draw' methods. + * Legend, frame and axes (though internally PlotObjects) are not included */ + public int getNumPlotObjects() { + return allPlotObjects.size(); + } + + /** Returns the number of PlotObjects fitting the mask. + * Legend, frame and axes (though internally PlotObjects) are not included */ + int getNumPlotObjects(int mask, boolean includeHidden) { + int nObjects = 0; + for (PlotObject plotObject : allPlotObjects) + if ((plotObject.type & mask) != 0 && (includeHidden || !plotObject.hasFlag(PlotObject.HIDDEN))) + nObjects++; + return nObjects; + } + + /** Gets an array with human-readable designations of the PlotObjects with types fitting the mask + * (i.e., 'mask' should be a bitwise or of the types desired) */ + String[] getPlotObjectDesignations(int mask, boolean includeHidden) { + int nObjects = getNumPlotObjects(mask, includeHidden); + String[] names = new String[nObjects]; + if (names.length == 0) return names; + int iData = 1, iArrow = 1, iLine = 1, iText = 1, iBox = 1, iShape = 1; //Human readable counters of each object type + int i = 0; + for (PlotObject plotObject : allPlotObjects) { + int type = plotObject.type; + if ((type & mask) == 0 || (!includeHidden && plotObject.hasFlag(PlotObject.HIDDEN))) continue; + String label = plotObject.label; + switch (type) { + case PlotObject.XY_DATA: + names[i] = "Data Set "+iData+": "+(plotObject.label != null ? + plotObject.label : "(" + plotObject.yValues.length + " data points)"); + iData++; + break; + case PlotObject.ARROWS: + names[i] = "Arrow Set "+iArrow+" ("+ plotObject.xValues.length + ")"; + iArrow++; + break; + case PlotObject.LINE: case PlotObject.NORMALIZED_LINE: case PlotObject.DOTTED_LINE: + String detail = ""; + if (type == PlotObject.DOTTED_LINE) detail = "dotted "; + if (plotObject.x ==plotObject.xEnd) detail += "vertical"; + else if (plotObject.y ==plotObject.yEnd) detail += "horizontal"; + if (detail.length()>0) detail = " ("+detail.trim()+")"; + names[i] = "Straight Line "+iLine+detail; + iLine++; + break; + case PlotObject.LABEL: case PlotObject.NORMALIZED_LABEL: + String text = plotObject.label.replaceAll("\n"," "); + if (text.length()>45) text = text.substring(0, 40)+"..."; + names[i] = "Text "+iText+": \""+text+'"'; + iText++; + break; + case PlotObject.SHAPES: + String s = plotObject.shapeType; + String[] words = s.split(" "); + names[i] = "Shapes (" + words[0] +") " + iShape; + iShape++; + break; + } + i++; + } + return names; + } + + /** Add the i-th PlotObject (in the sequence how they were added, including hidden ones) + * from another plot to this one. PlotObjects here refers to curves, arrows, labels etc. + * (not legend, axes and frame, though implemented as PlotObjects) + * Use 'update' to update the plot thereafter. + * @return Index of the plotObject added in the sequence they were added */ + public int addObjectFromPlot(Plot plot, int i) { + PlotObject plotObject = plot.getPlotObjectDeepClone(i); + plotObject.unsetFlag(PlotObject.CONSTRUCTOR_DATA); + allPlotObjects.add(plotObject); + int index = allPlotObjects.size() - 1; + return index; + } + + /** Get the style of the i-th PlotObject (curve, label, ...) in the sequence + * they were added (including hidden ones), as String with comma delimiters: + * Main Color, Secondary Color (or "none"), Line Width [, Symbol shape for XY_DATA] [,hidden] + * PlotObjects here refers to curves, arrows, labels etc. + * (not legend, exes and frame, though implemented as PlotObjects) */ + public String getPlotObjectStyle(int i) { + return getPlotObjectStyle(allPlotObjects.get(i)); + } + + String getPlotObjectStyle(PlotObject plotObject) { + String styleString = Colors.colorToString(plotObject.color) + "," + + Colors.colorToString(plotObject.color2) + "," + + plotObject.lineWidth; + if (plotObject.type == PlotObject.XY_DATA) + styleString += ","+SHAPE_NAMES[plotObject.shape]; + if (plotObject.hasFlag(PlotObject.HIDDEN)) + styleString += ",hidden"; + return styleString; + } + + /** Get the label the i-th PlotObject (in the sequence how they were added, including hidden ones). + * Returns null if no label. PlotObjects here refers to curves, arrows, labels etc. + * (not legend, exes and frame, though implemented as PlotObjects) */ + public String getPlotObjectLabel(int i) { + return allPlotObjects.get(i).label; + } + + /** Set the label the i-th PlotObject (in the sequence how they were added, including hidden ones) + * PlotObjects here refers to curves, arrows, labels etc. + * (not legend, exes and frame, though implemented as PlotObjects) */ + public void setPlotObjectLabel(int i, String label) { + allPlotObjects.get(i).label = label; + } + + /** Sets the style of the specified PlotObject (curve, label, etc.) from a + * comma-delimited string ("color1,color2,lineWidth[,symbol][,hidden]"), + * where "color2" can be "none" and "symbol" and "hidden" are optional. + * PlotObjects here refers to curves, arrows, labels etc. + * (not legend, exes and frame, though implemented as PlotObjects) */ + public void setStyle(int index, String style) { + if (index<0 || index>=allPlotObjects.size()) + throw new IllegalArgumentException("Index out of range"); + setPlotObjectStyle(allPlotObjects.get(index), style); + } + + public void setPlotObjectStyle(int i, String styleString) { + setStyle(i, styleString); + } + + void setPlotObjectStyle(PlotObject plotObject, String styleString) { + String[] items = styleString.split(","); + int nItems = items.length; + if (items[nItems-1].indexOf("hidden") >= 0) { + plotObject.setFlag(PlotObject.HIDDEN); + nItems = items.length - 1; + } else + plotObject.unsetFlag(PlotObject.HIDDEN); + plotObject.color = Colors.decode(items[0].trim(), plotObject.color); + plotObject.color2 = Colors.decode(items[1].trim(), null); + float lineWidth = plotObject.lineWidth; + if (items.length >= 3) try { + plotObject.lineWidth = Float.parseFloat(items[2].trim()); + } catch (NumberFormatException e) {}; + if (items.length >= 4 && plotObject.shape!=CUSTOM) + plotObject.shape = toShape(items[3].trim()); + updateImage(); + return; + } + + /** Returns the index of the first plot object with x,y data (points, line) or arrows + * with all data equal to those given. Returns or -1 is no such plot object exists. + * The array 'values' should contain the x, y, x error bar, yerror bar data. The 'values' array may have any size; + * only the data given are compared (e.g. for an array with length 2, there is no check for erro bars). + * Used when adding data from a table not to suggest the same data twice. */ + public int getPlotObjectIndex(float[][] values) { + return getPlotObjectIndex(PlotObject.XY_DATA|PlotObject.ARROWS, values); + } + + /** Returns the index of the first plot object fitting the type mask and with all data equal to those given. + * ('mask' should be a bitwise or of the types desired) + * Returns or -1 is no such plot object exists. + * The array 'values' should contain the x, y, x error bar, yerror bar data. The 'values' array may have any size; + * only the data given are compared (e.g. for an array with length 2, there is no check for erro bars). + * Used when adding data from a table not to suggest the same data twice. */ + int getPlotObjectIndex(int typeMask, float[][] values) { + for (int i=0; i(allPlotObjects.size()); + copyPlotObjectsVector(allPlotObjects, allPlotObjectsSnapshot); + } + + /** Restores the plot contents (not including axis formats etc) from the snapshot + * previously created by savePlotObjects(). See also killPlotObjectsSnapshot + * Use 'update' to update the plot thereafter. */ + public void restorePlotObjects() { + if (allPlotObjectsSnapshot != null) + copyPlotObjectsVector(allPlotObjectsSnapshot, allPlotObjects); + } + + /** Deletes the snapshot of the plot contents to make space */ + public void killPlotObjectsSnapshot() { + allPlotObjectsSnapshot = null; + } + + /** Creates a snapshot of the plot properties (formatting, range etc., not PlotObjects such as data and corresponding curves etc.), + * for later undo by restorePlotProperties. See also killPlotPropertiesSnapshot */ + public void savePlotPlotProperties() { + pp.rangeMinMax = currentMinMax; + ppSnapshot = pp.deepClone(); + } + + /** Restores the plot properties (formatting, range etc., not PlotObjects such as data and corresponding curves etc.) + * from a snapshot previously created by savePlotPlotProperties. See also killPlotPropertiesSnapshot. + * Use 'update' to update the plot thereafter. */ + public void restorePlotProperties() { + pp = ppSnapshot.deepClone(); + System.arraycopy(pp.rangeMinMax, 0, currentMinMax, 0, Math.min(pp.rangeMinMax.length, currentMinMax.length)); + } + + /** Deletes the snapshot of the plot properties to make space */ + public void killPlotPropertiesSnapshot() { + ppSnapshot = null; + } + + private void copyPlotObjectsVector(Vector src, Vectordest) { + if (dest.size() > 0) dest.removeAllElements(); + for (PlotObject plotObject : src) + dest.add(plotObject.deepClone()); + } + + PlotObject getPlotObjectDeepClone(int i) { + return allPlotObjects.get(i).deepClone(); + } + + /** Sets the plot range to the initial value determined from minima&maxima or given by setLimits. + * Updates the image if existing and updateImg is true */ + public void setLimitsToDefaults(boolean updateImg) { + saveMinMax(); + System.arraycopy(defaultMinMax, 0, currentMinMax, 0, defaultMinMax.length); + if (plotDrawn && updateImg) updateImage(); + } + + /** Sets the plot range to encompass all data. Updates the image if existing and updateImg is true. */ + public void setLimitsToFit(boolean updateImg) { + saveMinMax(); + currentMinMax = getMinAndMax(true, ALL_AXES_RANGE); + if (Double.isNaN(defaultMinMax[0]) && Double.isNaN(defaultMinMax[2])) //no range at all so far + System.arraycopy(currentMinMax, 0, defaultMinMax, 0, Math.min(currentMinMax.length, defaultMinMax.length)); + + enlargeRange(currentMinMax); //avoid points exactly at the border + //System.arraycopy(currentMinMax, 0, defaultMinMax, 0, currentMinMax.length); + if (plotDrawn && updateImg) updateImage(); + } + + /** reverts plot range to previous values and updates the image */ + public void setPreviousMinMax() { + if (Double.isNaN(savedMinMax[0])) return; //no saved values yet + double[] swap = new double[currentMinMax.length]; + System.arraycopy(currentMinMax, 0, swap, 0, currentMinMax.length); + System.arraycopy(savedMinMax, 0, currentMinMax, 0, currentMinMax.length); + System.arraycopy(swap, 0, savedMinMax, 0, currentMinMax.length); + updateImage(); + } + + /** Draws the plot (if not done before) in an ImageProcessor and returns the ImageProcessor with the plot. */ + public ImageProcessor getProcessor() { + draw(); + return ip; + } + + /** Returns the plot as an ImagePlus. + * If an ImagePlus for this plot already exists, displays the plot in that ImagePlus and returns it. */ + public ImagePlus getImagePlus() { + if (stack != null) { + if (imp != null) + return imp; + else { + imp = new ImagePlus(title, stack); + adjustCalibration(imp.getCalibration()); + return imp; + } + } + if (plotDrawn) + updateImage(); + else + draw(); + if (imp != null) { + if (imp.getProcessor() != ip) + imp.setProcessor(ip); + return imp; + } else { + ImagePlus imp = new ImagePlus(title, ip); + setImagePlus(imp); + return imp; + } + } + + /** Sets the ImagePlus where the plot will be displayed. If the ImagePlus is not + * known otherwise (e.g. from getImagePlus), this is needed for changes such as + * zooming in to work correctly. It also sets the calibration of the ImagePlus. + * The ImagePlus is not displayed or updated unless its ImageProcessor is + * no that of the current Plot (then it gets this ImageProcessor). + * Does nothing if imp is unchanged and has the ImageProcessor of this plot. + * 'imp' may be null to disconnect the plot from its ImagePlus. + * Does nothing for Plot Stacks. */ + public void setImagePlus(ImagePlus imp) { + if (imp != null && imp == this.imp && imp.getProcessor() == ip) + return; + if (stack != null) + return; + if (this.imp != null) + this.imp.setProperty(PROPERTY_KEY, null); + this.imp = imp; + if (imp != null) { + imp.setIgnoreGlobalCalibration(true); + adjustCalibration(imp.getCalibration()); + imp.setProperty(PROPERTY_KEY, this); + if (ip != null && imp.getProcessor() != ip) + imp.setProcessor(ip); + } + } + + /** Adjusts a Calibration object to fit the current axes. + * For log axes, the calibration refers to the base-10 logarithm of the value */ + public void adjustCalibration(Calibration cal) { + if (xMin == xMax) //tiff images can't handle infinity in scale, see TiffEncoder.writeScale + xScale = 1e6; + if (yMin == yMax) + yScale = 1e6; + cal.xOrigin = xBasePxl-xMin*xScale; + cal.pixelWidth = 1.0/Math.abs(xScale); //Calibration must not have negative pixel size + cal.yOrigin = yBasePxl+yMin*yScale; + cal.pixelHeight = 1.0/Math.abs(yScale); + cal.setInvertY(yScale >= 0); + cal.setXUnit(" "); // avoid 'pixels' for scaled units + if (xMin == xMax) + xScale = Double.POSITIVE_INFINITY; + if (yMin == yMax) + yScale = Double.POSITIVE_INFINITY; + } + + /** Displays the plot in a PlotWindow. + * Plot stacks are shown in a StackWindow, not in a PlotWindow; + * in this case the return value is null (use getImagePlus().getWindow() instead). + * Also returns null in BatchMode. Note that the PlotWindow might get closed + * immediately if its 'listValues' and 'autoClose' flags are set. + * @see #update() + */ + public PlotWindow show() { + PlotVirtualStack stack = getStack(); + if (stack!=null) { + getImagePlus().show(); + return null; + } + if ((IJ.macroRunning() && IJ.getInstance()==null) || Interpreter.isBatchMode()) { + imp = getImagePlus(); + imp.setPlot(this); + WindowManager.setTempCurrentImage(imp); + if (getMainCurveObject() != null) { + imp.setProperty("XValues", getXValues()); // Allows values to be retrieved by + imp.setProperty("YValues", getYValues()); // by Plot.getValues() macro function + } + Interpreter.addBatchModeImage(imp); + return null; + } + if (imp != null) { + Window win = imp.getWindow(); + if (win instanceof PlotWindow && win.isVisible()) { + updateImage(); // show in existing window + return (PlotWindow)win; + } else + setImagePlus(null); + } + PlotWindow pw = new PlotWindow(this); //note: this may set imp to null if pw has listValues and autoClose are set + if (IJ.isMacro() && imp!=null) // wait for plot to be displayed + IJ.selectWindow(imp.getID()); + return pw; + } + + /** + * Appends the current plot to a virtual stack and resets allPlotObjects + * for next slice + * N. Vischer + */ + public void addToStack() { + if (stack==null) + stack = new PlotVirtualStack(getSize().width,getSize().height); + draw(); + stack.addPlot(this); + IJ.showStatus("addToPlotStack: "+stack.size()); + allPlotObjects.clear(); + textLoc = null; + } + + public void appendToStack() { addToStack(); } + + /** Returns the virtual stack created by addToStack(). */ + public PlotVirtualStack getStack() { + IJ.showStatus(""); + return stack; + } + + /** Draws the plot specified for the first time. Does nothing if the plot has been drawn already. + * Call getProcessor to retrieve the ImageProcessor with it. + * Does no action with respect to the ImagePlus (if any) */ + public void draw() { + //IJ.log("draw(); plotDrawn="+plotDrawn); + if (plotDrawn) return; + getInitialMinAndMax(); + pp.frame.setFont(nonNullFont(pp.frame.getFont(), currentFont)); //make sure we have a number font for calculating the margins + getBlankProcessor(); + drawContents(ip); + } + + /** Freezes or unfreezes the plot. In the frozen state, the plot cannot be resized or updated, + * and the Plot class does no modifications to the ImageProcessor. + * Changes are recorded nevertheless and become effective with setFrozen(false). */ + public void setFrozen(boolean frozen) { + pp.isFrozen = frozen; + if (!pp.isFrozen) { // unfreeze operations ... + if (imp != null && ip != null) { + ImageCanvas ic = imp.getCanvas(); + if (ic instanceof PlotCanvas) { + ((PlotCanvas)ic).resetMagnification(); + imp.setTitle(imp.getTitle()); //update magnification in title + } + Undo.setup(Undo.TRANSFORM, imp); + } + updateImage(); + ImageWindow win = imp == null ? null : imp.getWindow(); + if (win != null) win.updateImage(imp); //show any changes made during the frozen state + } + } + + public boolean isFrozen() { + return pp.isFrozen; + } + + /** Draws the plot again, ignored if the plot has not been drawn before or the plot is frozen. */ + public void update() { + updateImage(); + } + + /** Draws the plot again, ignored if the plot has not been drawn before or the plot is frozen. + * If the ImagePlus exist, updates it and its calibration. */ + public void updateImage() { + if (!plotDrawn || pp.isFrozen) return; + getBlankProcessor(); + drawContents(ip); + if (imp == null || stack != null) return; + adjustCalibration(imp.getCalibration()); + imp.updateAndDraw(); + if (ip != imp.getProcessor()) + imp.setProcessor(ip); + } + + /** Returns the rectangle where the data are plotted. + * This rectangle includes the black outline frame at the top and left, but not at the bottom + * and right (when the frame is plotted with 1 pxl width). + * The image scale is its width or height in pixels divided by the data range in x or y. */ + public Rectangle getDrawingFrame() { + if (frame == null) + getBlankProcessor(); //setup frame if not done yet + return new Rectangle(frame.x, frame.y, frameWidth, frameHeight); + } + + /** Creates a new high-resolution plot by scaling it and displays that plot if showIt is true. + * title may be null, then a default title is used. */ + public ImagePlus makeHighResolution(String title, float scale, boolean antialiasedText, boolean showIt) { + Plot hiresPlot = null; + try { + hiresPlot = (Plot)clone(); //shallow clone, thus arrays&objects are not cloned, but they will be used only now + } catch (Exception e) {return null;} + hiresPlot.ip = null; + hiresPlot.imp = null; + hiresPlot.pp = pp.clone(); + if (!plotDrawn) hiresPlot.getInitialMinAndMax(); + hiresPlot.setScale(scale); + hiresPlot.setAntialiasedText(antialiasedText); + hiresPlot.defaultMinMax = currentMinMax.clone(); + ImageProcessor hiresIp = hiresPlot.getProcessor(); + if (title == null || title.length() == 0) + title = getTitle()+"_HiRes"; + title = WindowManager.makeUniqueName(title); + ImagePlus hiresImp = new ImagePlus(title, hiresIp); + Calibration cal = hiresImp.getCalibration(); + hiresPlot.adjustCalibration(cal); + if (showIt) { + hiresImp.setIgnoreGlobalCalibration(true); + hiresImp.show(); + } + hiresPlot.dispose(); //after drawing, we don't need the plot of the high-resolution image any more + return hiresImp; + } + + /** Releases the ImageProcessor and ImagePlus associated with the plot. + * May help garbage collection because some garbage collectors + * are said to be inefficient with circular references. */ + public void dispose() { + if (imp != null) + imp.setProperty(PROPERTY_KEY, null); + imp = null; + ip = null; + } + + /** Converts pixels to calibrated coordinates. In contrast to the image calibration, also + * works with log axes and inverted x axes */ + public double descaleX(int x) { + if (xMin == xMax) return xMin; + double xv = (x-xBasePxl)/xScale + xMin; + if (logXAxis) xv = Math.pow(10, xv); + return xv; + } + + /** Converts pixels to calibrated coordinates. In contrast to the image calibration, also + * works with log axes */ + public double descaleY(int y) { + if (yMin == yMax) return yMin; + double yv = (yBasePxl-y)/yScale +yMin; + if (logYAxis) yv = Math.pow(10, yv); + return yv; + } + + + /** Converts calibrated coordinates to pixel coordinates. In contrast to the image calibration, also + * works with log x axis and inverted x axis */ + public double scaleXtoPxl(double x) { + if (xMin == xMax) { + if (x==xMin) return xBasePxl; + else return x>xMin ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY; + } + if (logXAxis) + return xBasePxl+(Math.log10(x)-xMin)*xScale; + else + return xBasePxl+(x-xMin)*xScale; + } + + /** Converts calibrated coordinates to pixel coordinates. In contrast to the image calibration, also + * works with log y axis */ + public double scaleYtoPxl(double y) { + if (yMin == yMax) { + if (y==xMin) return yBasePxl; + else return y>yMin ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY; + } + if (logYAxis) + return yBasePxl-(Math.log10(y)-yMin)*yScale; + else + return yBasePxl-(y-yMin)*yScale; + } + + /** Calibrated coordinates to integer pixel coordinates */ + private int scaleX(double x) { + if (xMin == xMax) { + if (x==xMin) return xBasePxl; + else return x>xMin ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + if (logXAxis) + return xBasePxl+(int)Math.round((Math.log10(x)-xMin)*xScale); + else + return xBasePxl+(int)Math.round((x-xMin)*xScale); + } + + /** Converts calibrated coordinates to pixel coordinates. In contrast to the image calibration, also + * works with log axes */ + private int scaleY(double y) { + if (yMin == yMax) { + if (y==yMin) return yBasePxl; + else return y>yMin ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + if (logYAxis) + return yBasePxl-(int)Math.round((Math.log10(y)-yMin)*yScale); + else + return yBasePxl-(int)Math.round((y-yMin)*yScale); + } + + /** Converts calibrated coordinates to pixel coordinates. In contrast to the image calibration, also + * works with log axes and inverted x axes. Returns a large number instead NaN for log x axis and zero or negative x */ + private int scaleXWithOverflow(double x) { + if (!logXAxis || x>0) + return scaleX(x); + else + return xScale > 0 ? -1000000 : 1000000; + } + + /** Converts calibrated coordinates to pixel coordinates. In contrast to the image calibration, also + * works with log axes and inverted x axes. Returns a large number instead NaN for log y axis and zero or negative y */ + private int scaleYWithOverflow(double y) { + if (!logYAxis || y>0) + return scaleY(y); + else + return yScale > 0 ? 1000000 : -1000000; + } + + /** Scales a value of the original plot for a high-resolution plot. Returns an integer number of pixels >=1 */ + int sc(float length) { + int pixels = (int)(length*scale + 0.5); + if (pixels < 1) pixels = 1; + return pixels; + } + + /** Scales a font of the original plot for a high-resolution plot. */ + Font scFont(Font font) { + float size = font.getSize2D(); + return scale==1 ? font : font.deriveFont(size*scale); + } + + /** Returns whether the plot requires color (not grayscale) */ + boolean isColored() { + for (PlotObject plotObject : allPlotObjects) + if (isColored(plotObject.color) || isColored(plotObject.color2)) + return true; + for (PlotObject plotObject : pp.getAllPlotObjects()) + if (plotObject != null && (isColored(plotObject.color) || isColored(plotObject.color2))) + return true; + return false; + } + + /** Whether a color is non-grayscale, which requires color (not grayscale) for the plot */ + boolean isColored(Color c) { + if (c == null) return false; + return c.getRed() != c.getGreen() || c.getGreen() != c.getBlue(); + } + + /** Draws the plot contents (all PlotObjects and the frame and legend), without axes etc. */ + void drawContents(ImageProcessor ip) { + makeRangeGetSteps(); + ip.setColor(Color.black); + ip.setLineWidth(sc(1)); + float lineWidth = 1; + Color color = Color.black; + Font font = defaultFont; + + // draw all the plot objects in the sequence they were added, except for the one of the constructor + for (PlotObject plotObject : allPlotObjects) + if (!plotObject.hasFlag(PlotObject.CONSTRUCTOR_DATA)) { + //properties lineWidth, Font, Color set for one object remain for the next object unless changed + if (plotObject.lineWidth > 0) + lineWidth = plotObject.lineWidth; + else + plotObject.lineWidth = lineWidth; + if (plotObject.color != null) + color = plotObject.color; + else + plotObject.color = color; + if (plotObject.getFont() != null) + font = plotObject.getFont(); + else + plotObject.setFont(font); + //IJ.log("type="+plotObject.type+" color="+plotObject.color); + drawPlotObject(plotObject, ip); + } + + // draw the line passed with the constructor last, using the settings present when calling 'draw' + if (allPlotObjects.size()>0 && allPlotObjects.get(0).hasFlag(PlotObject.CONSTRUCTOR_DATA)) { + PlotObject mainPlotObject = allPlotObjects.get(0); + if (mainPlotObject.lineWidth == 0) + mainPlotObject.lineWidth = currentLineWidth == 0 ? 1 : currentLineWidth; + lineWidth = mainPlotObject.lineWidth; + if (mainPlotObject.color == null) + mainPlotObject.color = currentColor == null ? Color.black : currentColor; + drawPlotObject(mainPlotObject, ip); + } else { + if (currentLineWidth > 0) lineWidth = currentLineWidth; //linewidth when drawing determines frame linewidth + } + + // finally draw the frame & legend + if (!plotDrawn && pp.frame.lineWidth==DEFAULT_FRAME_LINE_WIDTH) { //when modifying PlotObjects styles later, don't change the frame line width any more + pp.frame.lineWidth = lineWidth; + if (pp.frame.lineWidth == 0) pp.frame.lineWidth = 1; + if (pp.frame.lineWidth > 3) pp.frame.lineWidth = 3; + } + ip.setLineWidth(sc(pp.frame.lineWidth)); + ip.setColor(pp.frame.color); + int x2 = frame.x + frame.width - 1; + int y2 = frame.y + frame.height - 1; + ip.moveTo(frame.x, frame.y); // draw the frame. Can't use ip.drawRect because it is inconsistent for different lineWidths + ip.lineTo(x2, frame.y); + ip.lineTo(x2, y2); + ip.lineTo(frame.x, y2); + ip.lineTo(frame.x, frame.y); + if (pp.legend != null && (pp.legend.flags & LEGEND_POSITION_MASK) != 0) + drawPlotObject(pp.legend, ip); + + plotDrawn = true; + } + + /** Creates the processor if not existing, clears the background and prepares + * it for plotting. Also called by the PlotWindow class to prepare the window. */ + ImageProcessor getBlankProcessor() { + makeMarginValues(); + //IJ.log("Plot.getBlankPr preferredH="+preferredPlotHeight+" pp.h="+pp.height); + if (pp.width <= 0 || pp.height <= 0) { + pp.width = sc(preferredPlotWidth) + leftMargin + rightMargin; + pp.height = sc(preferredPlotHeight) + topMargin + bottomMargin; + } + frameWidth = pp.width - (leftMargin + rightMargin); + frameHeight = pp.height - (topMargin + bottomMargin); + boolean isColored = isColored(); //color, not grayscale required? + if (ip == null || pp.width != ip.getWidth() || pp.height != ip.getHeight() || (isColored != (ip instanceof ColorProcessor))) { + if (isColored) { + ip = new ColorProcessor(pp.width, pp.height); + } else { + ip = new ByteProcessor(pp.width, pp.height); + invertedLut = Prefs.useInvertingLut && !Interpreter.isBatchMode() && IJ.getInstance()!=null; + if (invertedLut) ip.invertLut(); + } + if (imp != null && stack == null) + imp.setProcessor(ip); + } + if (ip instanceof ColorProcessor) + Arrays.fill((int[])(ip.getPixels()), 0xffffff); + else + Arrays.fill((byte[])(ip.getPixels()), invertedLut ? (byte)0 : (byte)0xff); + + ip.setFont(scFont(defaultFont)); + ip.setLineWidth(sc(1)); + ip.setAntialiasedText(pp.antialiasedText); + frame = new Rectangle(leftMargin, topMargin, frameWidth+1, frameHeight+1); + if (pp.frame.color2 != null) { //background color + ip.setColor(pp.frame.color2); + ip.setRoi(frame); + ip.fill(); + ip.resetRoi(); + } + ip.setColor(Color.black); + return ip; + } + + /** Calculates the margin sizes and sets the class variables accordingly */ + void makeMarginValues() { + Font font = nonNullFont(pp.frame.getFont(), currentFont); + float marginScale = 0.1f + 0.9f*font.getSize2D()/12f; + if (marginScale < 0.7f) marginScale = 0.7f; + if (marginScale > 2f) marginScale = 2f; + int addHspace = (int)Tools.getNumberFromList(pp.frame.options, "addhspace="); //user-defined extra space + int addVspace = (int)Tools.getNumberFromList(pp.frame.options, "addvspace="); + leftMargin = sc(LEFT_MARGIN*marginScale + addHspace); + rightMargin = sc(RIGHT_MARGIN*marginScale + addHspace); + topMargin = sc(TOP_MARGIN*marginScale + addVspace); + bottomMargin = sc(BOTTOM_MARGIN*marginScale + 2 + addVspace); + if(pp != null && pp.xLabel != null && pp.xLabel.getFont() != null){ + float numberSize = font.getSize2D(); + float labelSize = pp.xLabel.getFont().getSize2D(); + float extraHeight = 1.5f *(labelSize - numberSize); + if(extraHeight > 0){ + bottomMargin += sc(extraHeight); + leftMargin += sc(extraHeight); + } + } + } + + /** Calculate the actual range, major step interval and set variables for data <-> pixels scaling */ + double[] makeRangeGetSteps() { + steps = new double[2]; + logXAxis = hasFlag(X_LOG_NUMBERS); + logYAxis = hasFlag(Y_LOG_NUMBERS); + + for (int i=0; i MIN_LOG_RATIO || 1./rangeRatio > MIN_LOG_RATIO) || + !(currentMinMax[i] > 10*Float.MIN_VALUE) || !(currentMinMax[i+1] > 10*Float.MIN_VALUE)) + logAxis = false; + } + //for log axes, temporarily work on the logarithm + if (logAxis) { + currentMinMax[i] = Math.log10(currentMinMax[i]); + currentMinMax[i+1] = Math.log10(currentMinMax[i+1]); + } + // calculate grid or major tick interval + if ((i==0 && !simpleXAxis()) || (i==2 && !simpleYAxis())) { + int minGridspacing = i==0 ? MIN_X_GRIDSPACING : MIN_Y_GRIDSPACING; + int frameSize = i==0 ? frameWidth : frameHeight; + double step = Tools.getNumberFromList(pp.frame.options, i==0 ? "xinterval=" : "yinterval="); //user-defined interval + if (!Double.isNaN(step)) { + int nSteps = (int)(Math.floor(currentMinMax[i+1]/step+1e-10) - Math.ceil(currentMinMax[i]/step-1e-10)); + if (nSteps < 1) step = Double.NaN; //user-suppied interval too large, less than two numbers would be shown + if ((i==0 && nSteps*sc(minGridspacing)*0.5 > frameSize) || i!=0 && nSteps*sc(pp.frame.getFont().getSize()) > frameSize) + step = Double.NaN; //user-suppied interval too small, too many numbers would be shown + } + if (Double.isNaN(step)) { //automatic interval + step = Math.abs((currentMinMax[i+1] - currentMinMax[i]) * + Math.max(1.0/maxIntervals, (float)sc(minGridspacing)/frameSize+(maxIntervals>12 ? 0.02 : 0.06))); //the smallest allowable step + step = niceNumber(step); + } + if (logAxis && step < 1) + step = 1; + steps[i/2] = step; + //modify limits to grid or minor ticks if desired + boolean force2grid = hasFlag(i==0 ? X_FORCE2GRID : Y_FORCE2GRID) && !ignoreForce2Grid; + if (force2grid) { + int i1 = (int)Math.floor(Math.min(currentMinMax[i],currentMinMax[i+1])/step+1.e-10); + int i2 = (int)Math.ceil (Math.max(currentMinMax[i],currentMinMax[i+1])/step-1.e-10); + if (currentMinMax[i+1] > currentMinMax[i]) { // care about inverted axes with max= 0.999) { //don't snap on log axis if minor ticks are not full decades + // currentMinMax[i] = stepForSnap * Math.round(currentMinMax[i]/stepForSnap); + // currentMinMax[i+1] = stepForSnap * Math.round(currentMinMax[i+1]/stepForSnap); + // } + } + } + if (i==0) { + xMin = currentMinMax[i]; + xMax = currentMinMax[i+1]; + logXAxis = logAxis; + } else { + yMin = currentMinMax[i]; + yMax = currentMinMax[i+1]; + logYAxis = logAxis; + } + if (logAxis) { + currentMinMax[i] = Math.pow(10, currentMinMax[i]); + currentMinMax[i+1] = Math.pow(10, currentMinMax[i+1]); + } + } + //snapToMinorGrid = false; + ignoreForce2Grid = false; + + // calculate what we need to convert the data to screen pixels + xBasePxl = leftMargin; + yBasePxl = topMargin + frameHeight; + xScale = frameWidth/(xMax-xMin); + if (!(xMax-xMin!=0.0)) //if range==0 (all data the same), or NaN shift zero level so one can see the curve + xBasePxl += sc(10); + yScale = frameHeight/(yMax-yMin); + if (!(yMax-yMin!=0.0)) + yBasePxl -= sc(10); + //IJ.log("x,yScale="+(float)xScale+","+(float)yScale+" xMin,max="+(float)xMin+","+(float)xMax+" yMin.max="+(float)yMin+","+(float)yMax); + + drawAxesTicksGridNumbers(steps); + return steps; + } + + public void redrawGrid(){ + if (ip != null) { + ip.setColor(Color.black); + drawAxesTicksGridNumbers(steps); + ip.setColor(Color.black); + } + } + + /** Gets the initial plot limits (i.e., x&y ranges). For compatibility with previous versions of ImageJ, + * only the first PlotObject (with numeric data) is used to determine the limits. */ + void getInitialMinAndMax() { + int axisRangeFlags = 0; + if (Double.isNaN(defaultMinMax[0])) axisRangeFlags |= X_RANGE; + if (Double.isNaN(defaultMinMax[2])) axisRangeFlags |= Y_RANGE; + if (axisRangeFlags != 0) { + defaultMinMax = getMinAndMax(false, axisRangeFlags); + enlargeRange(defaultMinMax); + } + setLimitsToDefaults(false); //use the range values to start with, but don't draw yet + } + + /** Gets the minimum and maximum values from the first XY_DATA or ARROWS plotObject or all such plotObjects; + * axisRangeFlags determine for which axis to calculate the min&max (X_RANGE for x axis, Y_RANGE for y axis); + * for the other axes the limit is taken from defaultMinMax + * Array elements returned are xMin, xMax, yMin, yMax. Also sets enlargeRange to tell which limits should be enlarged + * beyond the minimum or maximum of the data */ + double[] getMinAndMax(boolean allObjects, int axisRangeFlags) { + boolean invertedXAxis = currentMinMax[1] < currentMinMax[0]; + boolean invertedYAxis = currentMinMax[3] < currentMinMax[2]; + double xSign = invertedXAxis ? -1 : 1; + double ySign = invertedYAxis ? -1 : 1; + double[] allMinMax = new double[]{xSign*Double.MAX_VALUE, -xSign*Double.MAX_VALUE, ySign*Double.MAX_VALUE, -ySign*Double.MAX_VALUE}; + for (int i=0; i>i/2) & 1)==0) //keep default min & max for this axis + allMinMax[i] = defaultMinMax[i]; + enlargeRange = new int[allMinMax.length]; + for (PlotObject plotObject : allPlotObjects) { + if ((plotObject.type == PlotObject.XY_DATA || plotObject.type == PlotObject.ARROWS) && !plotObject.hasFlag(PlotObject.HIDDEN)) { + getMinAndMax(allMinMax, enlargeRange, plotObject, axisRangeFlags); + if (!allObjects) break; + } + } + if ((axisRangeFlags & X_RANGE) != 0) { + String[] xCats = labelsInBraces('x'); // if we have categories at the axis, make some space for this text + if (xCats != null) { + allMinMax[0] = Math.min(allMinMax[0], -0.5); + allMinMax[1] = Math.min(allMinMax[1], xCats.length+0.5); + } + } + if ((axisRangeFlags & Y_RANGE) != 0) { + String[] yCats = labelsInBraces('y'); + if (yCats != null) { + allMinMax[2] = Math.min(allMinMax[2], -0.5); + allMinMax[3] = Math.min(allMinMax[3], yCats.length+0.5); + } + } + if (allMinMax[0]==Double.MAX_VALUE && allMinMax[1]==-Double.MAX_VALUE) { // no x values at all? keep previous + allMinMax[0] = defaultMinMax[0]; + allMinMax[1] = defaultMinMax[1]; + } + if (allMinMax[2]==Double.MAX_VALUE && allMinMax[3]==-Double.MAX_VALUE) { // no y values at all? keep previous + allMinMax[2] = defaultMinMax[2]; + allMinMax[3] = defaultMinMax[3]; + } + return allMinMax; + } + + /** Enlarges the current minimum and maximum ranges to include the data range of the last plotObject added, + * if it is an XY_DATA or ARROWS plotObject. + * Does not set the new limits as default, does not redraw the plot. */ + void fitRangeToLastPlotObject() { + if (allPlotObjects.size() < 1) return; + PlotObject plotObject = allPlotObjects.lastElement(); + if (Double.isNaN(currentMinMax[0]) || Double.isNaN(currentMinMax[2])) { // no range determined yet? + setLimitsToFit(false); + } else { //we have min&max already, just extend the range if necessary + enlargeRange = new int[currentMinMax.length]; + getMinAndMax(currentMinMax, enlargeRange, plotObject, ALL_AXES_RANGE); + enlargeRange(currentMinMax); + } + } + + /** Gets the minimum and maximum values from an XY_DATA or ARROWS plotObject; + * axisRangeFlags determine for which axis (X_RANGE for x axis, Y_RANGE for y axis) + * The minimum modifies allMinAndMax[0] (x), allMinAndMax[2] (y); the maximum modifies [1], [3]. + * If allMinAndMax values are modified, the corresponding enlargeRange array elements are also set */ + void getMinAndMax(double[] allMinAndMax, int[] enlargeRange, PlotObject plotObject, int axisRangeFlags) { + boolean invertedXAxis = currentMinMax[1] < currentMinMax[0]; + boolean invertedYAxis = currentMinMax[3] < currentMinMax[2]; + if (plotObject.type == PlotObject.XY_DATA) { + if ((axisRangeFlags & X_RANGE) != 0) { + int suggestedEnlarge = 0; + if (!(plotObject.shape == LINE || plotObject.shape == FILLED) || plotObject.yEValues != null) + suggestedEnlarge = ALWAYS_ENLARGE; //enlarge to make space at the obrders (we don't try to keep x=0 at the frame border) + getMinAndMax(allMinAndMax, enlargeRange, suggestedEnlarge, 0, plotObject.xValues, plotObject.xEValues, invertedXAxis); + if ((plotObject.shape == BAR || plotObject.shape == SEPARATED_BAR)&& plotObject.xValues.length > 1) { + int n = plotObject.xValues.length; + allMinAndMax[0] -= 0.5 * Math.abs(plotObject.xValues[1] - plotObject.xValues[0]); + allMinAndMax[1] += 0.5 * Math.abs(plotObject.xValues[n - 1] - plotObject.xValues[n - 2]); + } + } + if ((axisRangeFlags & Y_RANGE) != 0) { + int suggestedEnlarge = 0; + if (plotObject.shape==DOT || plotObject.xEValues != null) //these can't be seen if merging with the frame + suggestedEnlarge = ALWAYS_ENLARGE; + else if (!(plotObject.shape == LINE || plotObject.shape == FILLED)) + suggestedEnlarge = USUALLY_ENLARGE; + getMinAndMax(allMinAndMax, enlargeRange, suggestedEnlarge, 2, plotObject.yValues, plotObject.yEValues, invertedYAxis); + if ((plotObject.shape == BAR || plotObject.shape == SEPARATED_BAR) && + (allMinAndMax[2] > 0 && allMinAndMax[3]/allMinAndMax[2] >= 2) && !logYAxis) + allMinAndMax[2] = 0; // for bar plots, y min = 0 unless values differ less than a factor of 2 + } + } else if (plotObject.type == PlotObject.ARROWS) { + if ((axisRangeFlags & X_RANGE) != 0) { + getMinAndMax(allMinAndMax, enlargeRange, ALWAYS_ENLARGE, 0, plotObject.xValues, null, invertedXAxis); + getMinAndMax(allMinAndMax, enlargeRange, ALWAYS_ENLARGE, 0, plotObject.xEValues, null, invertedXAxis); + } + if ((axisRangeFlags & Y_RANGE) != 0) { + getMinAndMax(allMinAndMax, enlargeRange, ALWAYS_ENLARGE, 2, plotObject.yValues, null, invertedYAxis); + getMinAndMax(allMinAndMax, enlargeRange, ALWAYS_ENLARGE, 2, plotObject.yEValues, null, invertedYAxis); + } + } + } + + /** Gets the minimum and maximum values for a dataset (one direction, x or y), + * taking error bars (if not null) into account. + * The minimum modifies allMinAndMax[axisIndex] the maximum modifies allMinAndMax[axisIndex+1]. + * Also cares about whether the range should be enlarged to avoid hiding markers at the borders: + * suggestedEnlarge is 0 for lines or a suggestion for the data type; if the allMinAndMax is + * range is extended, the corresponding enlargeRange item is set accordingly */ + void getMinAndMax(double[] allMinAndMax, int[] enlargeRange, int suggestedEnlarge, + int axisIndex, float[] data, float[] errorBars, boolean invertedAxis) { + int nMinEqual = 0, nMaxEqual = 0; + int minIndex = invertedAxis ? axisIndex+1 : axisIndex; // index of 'min' value in allMinAndMax, enlargeRange + int maxIndex = invertedAxis ? axisIndex : axisIndex+1; + for (int i=0; i0 && i allMinAndMax[maxIndex]) { + allMinAndMax[maxIndex] = v2; + nMaxEqual = 1; + enlargeRange[maxIndex] = suggestedEnlarge; + if (suggestedEnlarge == 0 && ((i>0 && i10%) at min or max? Add extra space at borders ('usually', i.e. unless limit is zero) + if (enlargeRange[minIndex] == 0 && nMinEqual > 2 && nMinEqual*10 > data.length) + enlargeRange[minIndex] = USUALLY_ENLARGE; + if (enlargeRange[maxIndex] == 0 && nMaxEqual > 2 && nMaxEqual*10 > data.length) + enlargeRange[maxIndex] = USUALLY_ENLARGE; + //all data at min or max? Always add space to avoid hiding the line behind the frame + if (nMinEqual == data.length) + enlargeRange[minIndex] = ALWAYS_ENLARGE; + if (nMaxEqual == data.length) + enlargeRange[maxIndex] = ALWAYS_ENLARGE; + //same min or max as for current data set found already previously, but not asking yet for added space at borders? + if (nMinEqual>0 && enlargeRange[minIndex]0 && enlargeRange[maxIndex] frame.x && x < frame.x + frame.width; + boolean insideY = y > frame.y && y < frame.y + frame.height; + if (!insideX && !insideY) { + insideX = true; + insideY = true; + x = frame.x + frame.width / 2; + y = frame.y + frame.height / 2; + } + int leftPart = x - frame.x; + int rightPart = frame.x + frame.width - x; + int highPart = y - frame.y; + int lowPart = frame.y + frame.height - y; + + if (insideX) { + currentMinMax[0] = descaleX((int) (x - leftPart / zoomFactor)); + currentMinMax[1] = descaleX((int) (x + rightPart / zoomFactor)); + } + if (insideY) { + currentMinMax[2] = descaleY((int) (y + lowPart / zoomFactor)); + currentMinMax[3] = descaleY((int) (y - highPart / zoomFactor)); + } + updateImage(); + if (wasLogX != logXAxis ){//log-lin was automatically changed + int changedX = (int) scaleXtoPxl(plotX); + int left = changedX - leftPart; + int right = changedX + rightPart; + currentMinMax[0] = descaleX(left); + currentMinMax[1] = descaleX(right); + updateImage(); + } + if (wasLogY != logYAxis){//log-lin was automatically changed + int changedY = (int) scaleYtoPxl(plotY); + int bottom = changedY + lowPart; + int top = changedY + highPart; + currentMinMax[2] = descaleY(bottom); + currentMinMax[3] = descaleY(top); + updateImage(); + } + } + + /** Moves the plot range by a given number of pixels and updates the image */ + void scroll(int dx, int dy) { + if (logXAxis) { + currentMinMax[0] /= Math.pow(10, dx/xScale); + currentMinMax[1] /= Math.pow(10, dx/xScale); + } else { + currentMinMax[0] -= dx/xScale; + currentMinMax[1] -= dx/xScale; + } + if (logYAxis) { + currentMinMax[2] *= Math.pow(10, dy/yScale); + currentMinMax[3] *= Math.pow(10, dy/yScale); + } else { + currentMinMax[2] += dy/yScale; + currentMinMax[3] += dy/yScale; + } + updateImage(); + } + + /** Whether to draw simple axes without ticks, grid and numbers only for min, max*/ + private boolean simpleXAxis() { + return !hasFlag(X_TICKS | X_MINOR_TICKS | X_LOG_TICKS | X_GRID | X_NUMBERS); + } + + private boolean simpleYAxis() { + return !hasFlag(Y_TICKS | Y_MINOR_TICKS | Y_LOG_TICKS | Y_GRID | Y_NUMBERS); + } + + /** Draws ticks, grid and axis label for each tick/grid line. + * The grid or major tick spacing in each direction is given by steps */ + void drawAxesTicksGridNumbers(double[] steps) { + + if (ip==null) + return; + String[] xCats = labelsInBraces('x'); // create categories for the axes (if any) + String[] yCats = labelsInBraces('y'); + String multiplySymbol = getMultiplySymbol(); // for scientific notation + Font scFont = scFont(pp.frame.getFont()); + Font scFontMedium = scFont.deriveFont(scFont.getSize2D()*10f/12f); //for axis numbers if full size does not fit + Font scFontSmall = scFont.deriveFont(scFont.getSize2D()*9f/12f); //for subscripts + ip.setFont(scFont); + FontMetrics fm = ip.getFontMetrics(); + int fontAscent = fm.getAscent(); + ip.setJustification(LEFT); + // --- A l o n g X A x i s + int yOfXAxisNumbers = topMargin + frameHeight + fm.getHeight()*5/4 + sc(2); + if (hasFlag(X_NUMBERS | (logXAxis ? (X_TICKS | X_MINOR_TICKS) : X_LOG_TICKS) + X_GRID)) { + Font baseFont = scFont; + boolean majorTicks = logXAxis ? hasFlag(X_LOG_TICKS) : hasFlag(X_TICKS); + boolean minorTicks = hasFlag(X_MINOR_TICKS); + minorTicks = minorTicks && (xCats == null); + double step = steps[0]; + int i1 = (int)Math.ceil (Math.min(xMin, xMax)/step-1.e-10); + int i2 = (int)Math.floor(Math.max(xMin, xMax)/step+1.e-10); + int suggestedDigits = (int)Tools.getNumberFromList(pp.frame.options, "xdecimals="); //is not given, NaN cast to 0 + int digits = getDigits(xMin, xMax, step, 7, suggestedDigits); + int y1 = topMargin; + int y2 = topMargin + frameHeight; + if (xMin==xMax) { + if (hasFlag(X_NUMBERS)) { + String s = IJ.d2s(xMin,getDigits(xMin, 0.001*xMin, 5, suggestedDigits)); + int y = yBasePxl; + ip.drawString(s, xBasePxl-ip.getStringWidth(s)/2, yOfXAxisNumbers); + } + } else { + if (hasFlag(X_NUMBERS)) { + int w1 = ip.getStringWidth(IJ.d2s(currentMinMax[0], logXAxis ? -1 : digits)); + int w2 = ip.getStringWidth(IJ.d2s(currentMinMax[1], logXAxis ? -1 : digits)); + int wMax = Math.max(w1,w2); + if (wMax > Math.abs(step*xScale)-sc(8)) { + baseFont = scFontMedium; //small font if there is not enough space for the numbers + ip.setFont(baseFont); + } + } + + for (int i=0; i<=(i2-i1); i++) { + double v = (i+i1)*step; + int x = (int)Math.round((v - xMin)*xScale) + leftMargin; + + if (xCats!= null) { + int index = (int) v; + double remainder = Math.abs(v - Math.round(v)); + if(index >= 0 && index < xCats.length && remainder < 1e-9){ + String s = xCats[index]; + String[] parts = s.split("\n"); + int w = 0; + for(int jj = 0; jj < parts.length; jj++) + w = Math.max(w, ip.getStringWidth(parts[jj])); + + ip.drawString(s, x-w/2, yOfXAxisNumbers); + //ip.drawString(s, x-ip.getStringWidth(s)/2, yOfXAxisNumbers); + } + continue; + } + + if (hasFlag(X_GRID)) { + ip.setColor(gridColor); + ip.drawLine(x, y1, x, y2); + ip.setColor(Color.black); + } + if (majorTicks) { + ip.drawLine(x, y1, x, y1+sc(tickLength)); + ip.drawLine(x, y2, x, y2-sc(tickLength)); + } + if (hasFlag(X_NUMBERS)) { + if (logXAxis || digits<0) { + drawExpString(logXAxis ? Math.pow(10,v) : v, logXAxis ? -1 : -digits, + x, yOfXAxisNumbers-fontAscent/2, CENTER, fontAscent, baseFont, scFontSmall, multiplySymbol); + } else { + String s = IJ.d2s(v,digits); + ip.drawString(s, x-ip.getStringWidth(s)/2, yOfXAxisNumbers); + } + } + } + boolean haveMinorLogNumbers = i2-i1 < 2; //nunbers on log minor ticks only if < 2 decades + if (minorTicks && (!logXAxis || step > 1.1)) { //'standard' log minor ticks only for full decades + double mstep = niceNumber(step*0.19); //non-log: 4 or 5 minor ticks per major tick + double minorPerMajor = step/mstep; + if (Math.abs(minorPerMajor-Math.round(minorPerMajor)) > 1e-10) //major steps are not an integer multiple of minor steps? (e.g. user step 90 deg) + mstep = step/4; + if (logXAxis && mstep < 1) mstep = 1; + i1 = (int)Math.ceil (Math.min(xMin,xMax)/mstep-1.e-10); + i2 = (int)Math.floor(Math.max(xMin,xMax)/mstep+1.e-10); + for (int i=i1; i<=i2; i++) { + double v = i*mstep; + int x = (int)Math.round((v - xMin)*xScale) + leftMargin; + ip.drawLine(x, y1, x, y1+sc(minorTickLength)); + ip.drawLine(x, y2, x, y2-sc(minorTickLength)); + } + } else if (logXAxis && majorTicks && Math.abs(xScale)>sc(MIN_X_GRIDSPACING)) { //minor ticks for log + int minorNumberLimit = haveMinorLogNumbers ? (int)(0.12*Math.abs(xScale)/(fm.charWidth('0')+sc(2))) : 0; //more numbers on minor ticks when zoomed in + i1 = (int)Math.floor(Math.min(xMin,xMax)-1.e-10); + i2 = (int)Math.ceil (Math.max(xMin,xMax)+1.e-10); + for (int i=i1; i<=i2; i++) { + for (int m=2; m<10; m++) { + double v = i+Math.log10(m); + if (v > Math.min(xMin,xMax) && v < Math.max(xMin,xMax)) { + int x = (int)Math.round((v - xMin)*xScale) + leftMargin; + ip.drawLine(x, y1, x, y1+sc(minorTickLength)); + ip.drawLine(x, y2, x, y2-sc(minorTickLength)); + if (m<=minorNumberLimit) + drawExpString(Math.pow(10,v), 0, x, yOfXAxisNumbers-fontAscent/2, CENTER, + fontAscent, baseFont, scFontSmall, multiplySymbol); + } + } + } + } + } + } + // --- A l o n g Y A x i s + ip.setFont(scFont); + int maxNumWidth = 0; + int xNumberRight = leftMargin-sc(2)-ip.getStringWidth("0")/2; + Rectangle rect = ip.getStringBounds("0169"); + int yNumberOffset = -rect.y-rect.height/2; + if (hasFlag(Y_NUMBERS | (logYAxis ? (Y_TICKS | Y_MINOR_TICKS) : Y_LOG_TICKS) + Y_GRID)) { + ip.setJustification(RIGHT); + Font baseFont = scFont; + boolean majorTicks = logYAxis ? hasFlag(Y_LOG_TICKS) : hasFlag(Y_TICKS); + boolean minorTicks = logYAxis ? hasFlag(Y_LOG_TICKS) : hasFlag(Y_MINOR_TICKS); + minorTicks = minorTicks && (yCats == null); + double step = steps[1]; + int i1 = (int)Math.ceil (Math.min(yMin, yMax)/step-1.e-10); + int i2 = (int)Math.floor(Math.max(yMin, yMax)/step+1.e-10); + int suggestedDigits = (int)Tools.getNumberFromList(pp.frame.options, "ydecimals="); //is not given, NaN cast to 0 + int digits = getDigits(yMin, yMax, step, 5, suggestedDigits); + int x1 = leftMargin; + int x2 = leftMargin + frameWidth; + if (yMin==yMax) { + if (hasFlag(Y_NUMBERS)) { + String s = IJ.d2s(yMin,getDigits(yMin, 0.001*yMin, 5, suggestedDigits)); + maxNumWidth = ip.getStringWidth(s); + int y = yBasePxl; + ip.drawString(s, xNumberRight, y+fontAscent/2+sc(1)); + } + } else { + int digitsForWidth = logYAxis ? -1 : digits; + if (digitsForWidth < 0) { + digitsForWidth--; //"1.0*10^5" etc. needs more space than 1.0*5, simulate by adding one decimal + xNumberRight += sc(1)+ip.getStringWidth("0")/4; + } + String str1 = IJ.d2s(currentMinMax[2], digitsForWidth); + String str2 = IJ.d2s(currentMinMax[3], digitsForWidth); + if (digitsForWidth < 0) { + str1 = str1.replaceFirst("E",multiplySymbol); + str2 = str2.replaceFirst("E",multiplySymbol); + } + int w1 = ip.getStringWidth(str1); + int w2 = ip.getStringWidth(str2); + int wMax = Math.max(w1,w2); + if (hasFlag(Y_NUMBERS)) { + if (wMax > xNumberRight - sc(4) - (pp.yLabel.label.length()>0 ? fm.getHeight() : 0)) { + baseFont = scFontMedium; //small font if there is not enough space for the numbers + ip.setFont(baseFont); + } + } + //IJ.log(IJ.d2s(currentMinMax[2],digits)+": w="+w1+"; "+IJ.d2s(currentMinMax[3],digits)+": w="+w2+baseFont+" Space="+(leftMargin-sc(4+5)-fm.getHeight())); + for (int i=i1; i<=i2; i++) { + double v = step==0 ? yMin : i*step; + int y = topMargin + frameHeight - (int)Math.round((v - yMin)*yScale); + + if (yCats != null){ + int index = (int) v; + double remainder = Math.abs(v - Math.round(v)); + if(index >= 0 && index < yCats.length && remainder < 1e-9){ + String s = yCats[index]; + int multiLineOffset = 0; // multi-line cat labels + for(int jj = 0; jj < s.length(); jj++) + if(s.charAt(jj) == '\n') + multiLineOffset -= rect.height/2; + + ip.drawString(s, xNumberRight, y+yNumberOffset+ multiLineOffset); + } + continue; + } + + if (hasFlag(Y_GRID)) { + ip.setColor(gridColor); + ip.drawLine(x1, y, x2, y); + ip.setColor(Color.black); + } + if (majorTicks) { + ip.drawLine(x1, y, x1+sc(tickLength), y); + ip.drawLine(x2, y, x2-sc(tickLength), y); + } + if (hasFlag(Y_NUMBERS)) { + int w = 0; + if (logYAxis || digits<0) { + w = drawExpString(logYAxis ? Math.pow(10,v) : v, logYAxis ? -1 : -digits, + xNumberRight, y, RIGHT, fontAscent, baseFont, scFontSmall, multiplySymbol); + } else { + String s = IJ.d2s(v,digits); + w = ip.getStringWidth(s); + ip.drawString(s, xNumberRight, y+yNumberOffset); + } + if (w > maxNumWidth) maxNumWidth = w; + } + } + boolean haveMinorLogNumbers = i2-i1 < 2; //numbers on log minor ticks only if < 2 decades + if (minorTicks && (!logYAxis || step > 1.1)) { //'standard' log minor ticks only for full decades + double mstep = niceNumber(step*0.19); //non-log: 4 or 5 minor ticks per major tick + double minorPerMajor = step/mstep; + if (Math.abs(minorPerMajor-Math.round(minorPerMajor)) > 1e-10) //major steps are not an integer multiple of minor steps? (e.g. user step 90 deg) + mstep = step/4; + if (logYAxis && step < 1) mstep = 1; + i1 = (int)Math.ceil (Math.min(yMin,yMax)/mstep-1.e-10); + i2 = (int)Math.floor(Math.max(yMin,yMax)/mstep+1.e-10); + for (int i=i1; i<=i2; i++) { + double v = i*mstep; + int y = topMargin + frameHeight - (int)Math.round((v - yMin)*yScale); + ip.drawLine(x1, y, x1+sc(minorTickLength), y); + ip.drawLine(x2, y, x2-sc(minorTickLength), y); + } + } + if (logYAxis && majorTicks && Math.abs(yScale)>sc(MIN_X_GRIDSPACING)) { //minor ticks for log within the decade + int minorNumberLimit = haveMinorLogNumbers ? (int)(0.4*Math.abs(yScale)/fm.getHeight()) : 0; //more numbers on minor ticks when zoomed in + i1 = (int)Math.floor(Math.min(yMin,yMax)-1.e-10); + i2 = (int)Math.ceil(Math.max(yMin,yMax)+1.e-10); + for (int i=i1; i<=i2; i++) { + for (int m=2; m<10; m++) { + double v = i+Math.log10(m); + if (v > Math.min(yMin,yMax) && v < Math.max(yMin,yMax)) { + int y = topMargin + frameHeight - (int)Math.round((v - yMin)*yScale); + ip.drawLine(x1, y, x1+sc(minorTickLength), y); + ip.drawLine(x2, y, x2-sc(minorTickLength), y); + if (m<=minorNumberLimit) { + int w = drawExpString(Math.pow(10,v), 0, xNumberRight, y, RIGHT, + fontAscent, baseFont, scFontSmall, multiplySymbol); + if (w > maxNumWidth) maxNumWidth = w; + } + } + } + } + } + } + } + // --- Write min&max of range if simple style without any axis format flags + ip.setFont(scFont); + ip.setJustification(LEFT); + String xLabelToDraw = pp.xLabel.label; + String yLabelToDraw = pp.yLabel.label; + if (simpleYAxis()) { // y-axis min&max + int digits = getDigits(yMin, yMax, 0.001*(yMax-yMin), 6, 0); + String s = IJ.d2s(yMax, digits); + int sw = ip.getStringWidth(s); + if ((sw+sc(4)) > leftMargin) + ip.drawString(s, sc(4), topMargin-sc(4)); + else + ip.drawString(s, leftMargin-ip.getStringWidth(s)-sc(4), topMargin+10); + s = IJ.d2s(yMin, digits); + sw = ip.getStringWidth(s); + if ((sw+4)>leftMargin) + ip.drawString(s, sc(4), topMargin+frame.height); + else + ip.drawString(s, leftMargin-ip.getStringWidth(s)-sc(4), topMargin+frame.height); + if (logYAxis) yLabelToDraw += " (LOG)"; + } + int y = yOfXAxisNumbers; + if (simpleXAxis()) { // x-axis min&max + int digits = getDigits(xMin, xMax, 0.001*(xMax-xMin), 7, 0); + ip.drawString(IJ.d2s(xMin,digits), leftMargin, y); + String s = IJ.d2s(xMax,digits); + ip.drawString(s, leftMargin + frame.width-ip.getStringWidth(s)+6, y); + y -= fm.getHeight(); + if (logXAxis) xLabelToDraw += " (LOG)"; + } else + y += sc(1); + // --- Write x and y axis text labels + if (xCats == null) { + ip.setFont(pp.xLabel.getFont() == null ? scFont : scFont(pp.xLabel.getFont())); + ImageProcessor xLabel = stringToPixels(xLabelToDraw); + if(xLabel != null){ + int xpos = leftMargin+(frame.width-xLabel.getWidth())/2; + int ypos = y + scFont.getSize()/3;//topMargin + frame.height + bottomMargin-xLabel.getHeight(); + ip.insert(xLabel, xpos, ypos); + } + } + if (yCats == null) { + ip.setFont(pp.yLabel.getFont() == null ? scFont : scFont(pp.yLabel.getFont())); + ImageProcessor yLabel = stringToPixels(yLabelToDraw); + if(yLabel != null){ + yLabel = yLabel.rotateLeft(); + int xRightOfYLabel = xNumberRight - maxNumWidth - sc(2); + int xpos = xRightOfYLabel - yLabel.getWidth() - sc(2); + int ypos = topMargin + (frame.height -yLabel.getHeight())/2; + ip.insert(yLabel, xpos, ypos); + } + } + } + + /** Returns the array of categories from an axis label in the form {cat1,cat2,cat3}, or null if not this form + * @param labelCode can be 'x' or 'y', for the x or y axis label*/ + String[] labelsInBraces(char labelCode) { + String s = getLabel(labelCode); + if (s.startsWith("{") && s.endsWith("}")) { + String inBraces = s.substring(1, s.length() - 1); + String[] catLabels = inBraces.split(","); + return catLabels; + } else { + return null; + } + } + + /** Returns the smallest "nice" number >= v. "Nice" numbers are .. 0.5, 1, 2, 5, 10, 20 ... */ + double niceNumber(double v) { + double base = Math.pow(10,Math.floor(Math.log10(v)-1.e-6)); + if (v > 5.0000001*base) return 10*base; + else if (v > 2.0000001*base) return 5*base; + else return 2*base; + } + + /** draw something like 1.2 10^-9; returns the width of the string drawn. + * 'Digits' should be >=0 for drawing the mantissa (=1.38 in this example), negative to draw only 10^exponent + * Currently only supports center justification and right justification (y of center line) + * Fonts baseFont, smallFont should be scaled already + * Returns the width of the String */ + int drawExpString(double value, int digits, int x, int y, int justification, + int fontAscent, Font baseFont, Font smallFont, String multiplySymbol) { + String base = "10"; + String exponent = null; + String s = IJ.d2s(value, digits<=0 ? -1 : -digits); + if (Tools.parseDouble(s) == 0) s = "0"; //don't write 0 as 0*10^0 + int ePos = s.indexOf('E'); + if (ePos < 0) + base = s; //can't have exponential format, e.g. NaN + else { + if (digits>=0) { + base = s.substring(0,ePos); + if (digits == 0) + base = Integer.toString((int)Math.round(Tools.parseDouble(base))); + base += multiplySymbol+"10"; + } + exponent = s.substring(ePos+1); + } + //IJ.log(s+" -> "+base+"^"+exponent+" maxAsc="+fontAscent+" font="+baseFont); + ip.setJustification(RIGHT); + int width = ip.getStringWidth(base); + if (exponent != null) { + ip.setFont(smallFont); + int wExponent = ip.getStringWidth(exponent); + width += wExponent; + if (justification == CENTER) x += width/2; + ip.drawString(exponent, x, y+fontAscent*3/10); + x -= wExponent; + ip.setFont(baseFont); + } + ip.drawString(base, x, y+fontAscent*7/10); + return width; + } + + /** Returns the user-supplied (via setOptions) or default multiplication symbol (middot) */ + String getMultiplySymbol() { + String multiplySymbol = Tools.getStringFromList(pp.frame.options, "msymbol="); + if (multiplySymbol==null) + multiplySymbol = Tools.getStringFromList(pp.frame.options, "multiplysymbol="); + return multiplySymbol != null ? multiplySymbol : MULTIPLY_SYMBOL; + } + + //Returns a pixelMap containting labelStr. + //Uses font of current ImageProcessor. + //Returns null for empty or blank-only strings + //Supports !!subscript!! and ^^superscript^^ + ByteProcessor stringToPixels(String labelStr) { + Font bigFont = ip.getFont(); + Rectangle rect = ip.getStringBounds(labelStr); + int ww = rect.width * 2; + int hh = rect.height * 3;//enough space, will be cropped later + int y0 = rect.height * 2;//base line + if (ww <= 0 || hh <= 0) { + return null; + } + ByteProcessor box = new ByteProcessor(ww, hh); + box.setColor(Color.WHITE); + //box.setColor(Color.LIGHT_GRAY); //make box visible for test + box.fill(); + box.setColor(Color.black); + box.setAntialiasedText(pp.antialiasedText); + if (invertedLut) { + box.invertLut(); + } + box.setFont(bigFont); + + FontMetrics fm = box.getFontMetrics(); + int ascent = fm.getAscent(); + int offSub = ascent / 6; + int offSuper = -ascent / 2; + Font smallFont = bigFont.deriveFont((float) (bigFont.getSize() * 0.7)); + + Rectangle bigBounds = box.getStringBounds(labelStr); + boolean doParse = (labelStr.indexOf("^^") >= 0 || labelStr.indexOf("!!") >= 0); + doParse = doParse && (labelStr.indexOf("^^^") < 0 && labelStr.indexOf("!!!") < 0); + if (!doParse) { + box.drawString(labelStr, 0, y0); + Rectangle cropRect = new Rectangle(bigBounds); + cropRect.y += y0; + box.setRoi(cropRect); + ImageProcessor boxI = box.crop(); + box = boxI.convertToByteProcessor(); + return box; + } + + if (labelStr.endsWith("^^") || labelStr.endsWith("!!")) { + labelStr = labelStr.substring(0, labelStr.length() - 2); + } + if (labelStr.startsWith("^^") || labelStr.startsWith("!!")) { + labelStr = " " + labelStr; + } + + box.setFont(smallFont); + Rectangle smallBounds = box.getStringBounds(labelStr); + box.setFont(bigFont); + int upperBound = y0 + smallBounds.y + offSuper; + int lowerBound = y0 + smallBounds.y + smallBounds.height + offSub; + + int h = fm.getHeight(); + int len = labelStr.length(); + int[] tags = new int[len]; + int nTags = 0; + + for (int jj = 0; jj < len - 2; jj++) {//get positions where font size changes + if (labelStr.substring(jj, jj + 2).equals("^^")) { + tags[nTags++] = jj; + } + if (labelStr.substring(jj, jj + 2).equals("!!")) { + tags[nTags++] = -jj; + } + } + tags[nTags++] = len; + tags = Arrays.copyOf(tags, nTags); + + int leftIndex = 0; + int xRight = 0; + int y2 = y0; + + boolean subscript = labelStr.startsWith("!!"); + for (int pp = 0; pp < tags.length; pp++) {//draw all text fragments + int rightIndex = tags[pp]; + rightIndex = Math.abs(rightIndex); + String part = labelStr.substring(leftIndex, rightIndex); + boolean small = pp % 2 == 1;//toggle odd/even + if (small) { + box.setFont(smallFont); + if (subscript) { + y2 = y0 + offSub; + } else {//superscript: + y2 = y0 + offSuper; + } + } else { + box.setFont(bigFont); + y2 = y0; + } + xRight++; + int partWidth = box.getStringWidth(part); + box.drawString(part, xRight, y2); + leftIndex = rightIndex + 2; + subscript = tags[pp] < 0;//negative positions = subscript + xRight += partWidth; + } + xRight += h / 4; + Rectangle cropRect = new Rectangle(0, upperBound, xRight, lowerBound - upperBound); + box.setRoi(cropRect); + ImageProcessor boxI = box.crop(); + box = boxI.convertToByteProcessor(); + return box; + } + + /** Returns the number of digits to display the number n with resolution 'resolution'; + * (if n is integer and small enough to display without scientific notation, + * no decimals are needed, irrespective of 'resolution') + * Scientific notation is used for more than 'maxDigits' (must be >=3), and indicated + * by a negative return value, or if suggestedDigits is negative + * Returns 'suggestedDigits' if not 0 and compatible with the resolution; negative values of + * 'suggestedDigits' switch to scientific notation. */ + static int getDigits(double n, double resolution, int maxDigits, int suggestedDigits) { + if (n==Math.round(n) && Math.abs(n) < Math.pow(10,maxDigits-1)-1) //integers and not too big + return suggestedDigits; + else + return getDigits2(n, resolution, maxDigits, suggestedDigits); + } + + /** Number of digits to display the range between n1 and n2 with resolution 'resolution'; + * Scientific notation is used for more than 'maxDigits' (must be >=3), and indicated + * by a negative return value + * Returns 'suggestedDigits' if not 0 and compatible with the resolution; negative values of + * 'suggestedDigits' switch to sceintific notation. */ + static int getDigits(double n1, double n2, double resolution, int maxDigits, int suggestedDigits) { + if (n1==0 && n2==0) return suggestedDigits; + return getDigits2(Math.max(Math.abs(n1),Math.abs(n2)), resolution, maxDigits, suggestedDigits); + } + + static int getDigits2(double n, double resolution, int maxDigits, int suggestedDigits) { + if (Double.isNaN(n) || Double.isInfinite(n)) + return 0; //no scientific notation + int log10ofN = (int)Math.floor(Math.log10(Math.abs(n))+1e-7); + int digits = resolution != 0 ? + -(int)Math.floor(Math.log10(Math.abs(resolution))+1e-7) : + Math.max(0, -log10ofN+maxDigits-2); + int sciDigits = -Math.max((log10ofN+digits),1); + //IJ.log("n="+(float)n+"digitsRaw="+digits+" log10ofN="+log10ofN+" sciDigits="+sciDigits); + if ((digits < -2 && log10ofN >= maxDigits) || suggestedDigits < 0) + digits = sciDigits; //scientific notation for large numbers or if desired via suggestedDigits (plot.setOptions) + else if (digits < 0) + digits = 0; + else if (digits > maxDigits-1 && log10ofN < -2) + digits = sciDigits; // scientific notation for small numbers + return digits < 0 ? Math.min(sciDigits, suggestedDigits) : Math.max(digits, suggestedDigits); + } + + static boolean isInteger(double n) { + return n==Math.round(n); + } + + private void drawPlotObject(PlotObject plotObject, ImageProcessor ip) { + //IJ.log("DRAWING type="+plotObject.type+" lineWidth="+plotObject.lineWidth+" shape="+plotObject.shape); + if (plotObject.hasFlag(PlotObject.HIDDEN)) return; + ip.setColor(plotObject.color); + ip.setLineWidth(sc(plotObject.lineWidth)); + int type = plotObject.type; + switch (type) { + case PlotObject.XY_DATA: + ip.setClipRect(frame); + int nPoints = Math.min(plotObject.xValues.length, plotObject.yValues.length); + + if (plotObject.shape==BAR || plotObject.shape==SEPARATED_BAR) + drawBarChart(plotObject); // (separated) bars + + if (plotObject.shape == FILLED) { // filling below line + ip.setColor(plotObject.color2 != null ? plotObject.color2 : plotObject.color); + drawFloatPolyLineFilled(ip, plotObject.xValues, plotObject.yValues, nPoints); + } + ip.setColor(plotObject.color); + ip.setLineWidth(sc(plotObject.lineWidth)); + + if (plotObject.yEValues != null) // error bars in front of bars and fill area below the line, but behind lines and marker symbols + drawVerticalErrorBars(plotObject.xValues, plotObject.yValues, plotObject.yEValues); + if (plotObject.xEValues != null) + drawHorizontalErrorBars(plotObject.xValues, plotObject.yValues, plotObject.xEValues); + + if (plotObject.hasFilledMarker()) { // fill markers with secondary color + int markSize = plotObject.getMarkerSize(); + ip.setColor(plotObject.color2); + ip.setLineWidth(1); + for (int i=0; i0) && (!logYAxis || plotObject.yValues[i]>0) + && !Double.isNaN(plotObject.xValues[i]) && !Double.isNaN(plotObject.yValues[i])) + fillShape(plotObject.shape, scaleX(plotObject.xValues[i]), scaleY(plotObject.yValues[i]), markSize); + ip.setColor(plotObject.color); + ip.setLineWidth(sc(plotObject.lineWidth)); + } + if (plotObject.hasCurve()) { // draw the lines between the points + if (plotObject.shape == CONNECTED_CIRCLES) + ip.setColor(plotObject.color2 == null ? Color.black : plotObject.color2); + drawFloatPolyline(ip, plotObject.xValues, plotObject.yValues, nPoints); + ip.setColor(plotObject.color); + } + if (plotObject.hasMarker()) { // draw the marker symbols + int markSize = plotObject.getMarkerSize(); + ip.setColor(plotObject.color); + Font saveFont = ip.getFont(); + for (int i=0; i0) && (!logYAxis || plotObject.yValues[i]>0) + && !Double.isNaN(plotObject.xValues[i]) && !Double.isNaN(plotObject.yValues[i])) + drawShape(plotObject, scaleX(plotObject.xValues[i]), scaleY(plotObject.yValues[i]), markSize, i); + } + if (plotObject.shape==CUSTOM) + ip.setFont(saveFont); + } + ip.setClipRect(null); + break; + case PlotObject.ARROWS: + ip.setClipRect(frame); + for (int i=0; i sc(MAX_ARROWHEAD_LENGTH)) arrowHeadLength = sc(MAX_ARROWHEAD_LENGTH); + if (arrowHeadLength < sc(MIN_ARROWHEAD_LENGTH)) arrowHeadLength = sc(MIN_ARROWHEAD_LENGTH); + drawArrow(xt1, yt1, xt2, yt2, arrowHeadLength); + } + } + ip.setClipRect(null); + break; + + case PlotObject.SHAPES: + int iBoxWidth = 20; + ip.setClipRect(frame); + String shType = plotObject.shapeType.toLowerCase(); + if (shType.contains("rectangles")) { + int nShapes = plotObject.shapeData.size(); + + for (int i = 0; i < nShapes; i++) { + float[] corners = (float[])(plotObject.shapeData.get(i)); + int x1 = scaleX(corners[0]); + int y1 = scaleY(corners[1]); + int x2 = scaleX(corners[2]); + int y2 = scaleY(corners[3]); + + ip.setLineWidth(sc(plotObject.lineWidth)); + int left = Math.min(x1, x2); + int right = Math.max(x1, x2); + int top = Math.min(y1, y2); + int bottom = Math.max(y1, y2); + + Rectangle r1 = new Rectangle(left, top, right-left, bottom - top); + Rectangle cBox = frame.intersection(r1); + if (plotObject.color2 != null) { + ip.setColor(plotObject.color2); + ip.fillRect(cBox.x, cBox.y, cBox.width, cBox.height); + } + ip.setColor(plotObject.color); + ip.drawRect(cBox.x, cBox.y, cBox.width, cBox.height); + } + ip.setClipRect(null); + break; + } + if (shType.equals("redraw_grid")) { + ip.setLineWidth(sc(1)); + redrawGrid(); + ip.setClipRect(null); + break; + } + if (shType.contains("boxes")) { + + String[] parts = Tools.split(shType); + for (int jj = 0; jj < parts.length; jj++) { + String[] pairs = parts[jj].split("="); + if ((pairs.length == 2) && pairs[0].equals("width")) { + iBoxWidth = Integer.parseInt(pairs[1]); + } + } + boolean horizontal = shType.contains("boxesx"); + int nShapes = plotObject.shapeData.size(); + int halfWidth = Math.round(sc(iBoxWidth / 2)); + for (int i = 0; i < nShapes; i++) { + + float[] coords = (float[])(plotObject.shapeData.get(i)); + + if (!horizontal) { + + int x = scaleX(coords[0]); + int y1 = scaleY(coords[1]); + int y2 = scaleY(coords[2]); + int y3 = scaleY(coords[3]); + int y4 = scaleY(coords[4]); + int y5 = scaleY(coords[5]); + ip.setLineWidth(sc(plotObject.lineWidth)); + + Rectangle r1 = new Rectangle(x - halfWidth, y4, halfWidth * 2, y2 - y4); + Rectangle cBox = frame.intersection(r1); + if (y1 != y2 || y4 != y5)//otherwise omit whiskers + { + ip.drawLine(x, y1, x, y5);//whiskers + } + if (plotObject.color2 != null) { + ip.setColor(plotObject.color2); + ip.fillRect(cBox.x, cBox.y, cBox.width, cBox.height); + } + ip.setColor(plotObject.color); + ip.drawRect(cBox.x, cBox.y, cBox.width, cBox.height); + ip.setClipRect(frame); + ip.drawLine(x - halfWidth, y3, x + halfWidth - 1, y3); + } + + if (horizontal) { + + int y = scaleY(coords[0]); + int x1 = scaleX(coords[1]); + int x2 = scaleX(coords[2]); + int x3 = scaleX(coords[3]); + int x4 = scaleX(coords[4]); + int x5 = scaleX(coords[5]); + ip.setLineWidth(sc(plotObject.lineWidth)); + if(x1 !=x2 || x4 != x5)//otherwise omit whiskers + ip.drawLine(x1, y, x5, y);//whiskers + Rectangle r1 = new Rectangle(x2, y - halfWidth, x4 - x2, halfWidth * 2); + Rectangle cBox = frame.intersection(r1); + if (plotObject.color2 != null) { + ip.setColor(plotObject.color2); + ip.fillRect(cBox.x, cBox.y, cBox.width, cBox.height); + } + ip.setColor(plotObject.color); + ip.drawRect(cBox.x, cBox.y, cBox.width, cBox.height); + ip.setClipRect(frame); + ip.drawLine(x3, y - halfWidth, x3, y + halfWidth - 1); + } + } + ip.setClipRect(null); + break; + } + case PlotObject.LINE: + ip.setClipRect(frame); + ip.drawLine(scaleX(plotObject.x), scaleY(plotObject.y), scaleX(plotObject.xEnd), scaleY(plotObject.yEnd)); + ip.setClipRect(null); + break; + case PlotObject.NORMALIZED_LINE: + ip.setClipRect(frame); + int ix1 = leftMargin + (int)(plotObject.x*frameWidth); + int iy1 = topMargin + (int)(plotObject.y*frameHeight); + int ix2 = leftMargin + (int)(plotObject.xEnd*frameWidth); + int iy2 = topMargin + (int)(plotObject.yEnd*frameHeight); + ip.drawLine(ix1, iy1, ix2, iy2); + ip.setClipRect(null); + break; + case PlotObject.DOTTED_LINE: + ip.setClipRect(frame); + ix1 = scaleX(plotObject.x); + iy1 = scaleY(plotObject.y); + ix2 = scaleX(plotObject.xEnd); + iy2 = scaleY(plotObject.yEnd); + double length = calculateDistance(ix1, ix2, iy1, iy2) + 0.1; + int n = (int)(length/plotObject.step); + for (int i = 0; i<=n; i++) + ip.drawDot(ix1 + (int)Math.round((ix2-ix1)*(double)i/n), iy1 + (int)Math.round((iy2-iy1)*(double)i/n)); + ip.setClipRect(null); + break; + case PlotObject.LABEL: + case PlotObject.NORMALIZED_LABEL: + ip.setJustification(plotObject.justification); + if (plotObject.getFont() != null) + ip.setFont(scFont(plotObject.getFont())); + int xt = type==PlotObject.LABEL ? scaleX(plotObject.x) : leftMargin + (int)(plotObject.x*frameWidth); + int yt = type==PlotObject.LABEL ? scaleY(plotObject.y) : topMargin + (int)(plotObject.y*frameHeight); + ip.drawString(plotObject.label, xt, yt); + break; + case PlotObject.LEGEND: + drawLegend(plotObject, ip); + break; + } + } + + /** Draw a bar at each point */ + void drawBarChart(PlotObject plotObject) { + int n = Math.min(plotObject.xValues.length, plotObject.yValues.length); + String[] xCats = labelsInBraces('x'); // do we have categories at the x axis instead of numbers? + boolean separatedBars = plotObject.shape == SEPARATED_BAR || xCats != null; + int halfBarWidthInPixels = n <= 1 ? Math.max(1, frameWidth/2-2) : 0; + if (separatedBars && n > 1) + halfBarWidthInPixels = Math.max(1, (int)Math.round(Math.abs + (0.5*(plotObject.xValues[n-1] - plotObject.xValues[0])/(n-1) * xScale * SEPARATED_BAR_WIDTH))); + int y0 = scaleYWithOverflow(0); + boolean yZeroInFrame = !logYAxis && yBasePxl>frame.y && yBasePxl 0 ? 0.5f*(plotObject.xValues[i-1]+plotObject.xValues[i]) : + 1.5f*plotObject.xValues[i] - 0.5f*plotObject.xValues[i+1]); + right = scaleX(i < n-1 ? 0.5f*(plotObject.xValues[i]+plotObject.xValues[i+1]) : + 1.5f*plotObject.xValues[i] - 0.5f*plotObject.xValues[i-1]); + } else { + int x = scaleX(plotObject.xValues[i]); + left = x - halfBarWidthInPixels; //separated bars or n<=1 : fixed bar width + right = x + halfBarWidthInPixels; + } + if (left < frame.x) left = frame.x; + if (left > frame.x+frame.width) left = frame.x+frame.width; + if (right < frame.x) right = frame.x; + if (right > frame.x+frame.width) right = frame.x+frame.width; + int y = scaleYWithOverflow(plotObject.yValues[i]); + if (plotObject.color2 != null) { + ip.setColor(plotObject.color2); + for (int x2 = Math.min(left,right); x2 <= Math.max(left,right); x2++) + ip.drawLine(x2, y0, x2, y); //cant use ip.fillRect (ignores the clipRect), so we it fill line by line + } + ip.setColor(plotObject.color); + ip.setLineWidth(sc(plotObject.lineWidth)); + if (separatedBars) { + ip.drawLine(left, y0, left, y); //up + ip.drawLine(left, y, right, y); //right + ip.drawLine(right, y, right, y0); //down + if (yZeroInFrame) + ip.drawLine(left, y0, right, y0);//baseline + } else { + ip.drawLine(left, prevY, left, y); //up or down + ip.drawLine(left, y, right, y); //right + if (i == n - 1) + ip.drawLine(right, y, right, y0);//last down + prevY = y; + } + } + } + + /** Draw the symbol for the data point number 'pointIndex' (pointIndex < 0 when drawing the legend) */ + void drawShape(PlotObject plotObject, int x, int y, int size, int pointIndex) { + int shape = plotObject.shape; + if (shape == DIAMOND) size = (int)(size*1.21); + int xbase = x-sc(size/2); + int ybase = y-sc(size/2); + int xend = x+sc(size/2); + int yend = y+sc(size/2); + if (ip==null) + return; + switch(shape) { + case X: + ip.drawLine(xbase,ybase,xend,yend); + ip.drawLine(xend,ybase,xbase,yend); + break; + case BOX: + ip.drawLine(xbase,ybase,xend,ybase); + ip.drawLine(xend,ybase,xend,yend); + ip.drawLine(xend,yend,xbase,yend); + ip.drawLine(xbase,yend,xbase,ybase); + break; + case TRIANGLE: + ip.drawLine(x,ybase-sc(1),xend+sc(1),yend); //height must be odd, otherwise rounding leads to asymmetric shape + ip.drawLine(x,ybase-sc(1),xbase-sc(1),yend); + ip.drawLine(xend+sc(1),yend,xbase-sc(1),yend); + break; + case CROSS: + ip.drawLine(xbase,y,xend,y); + ip.drawLine(x,ybase,x,yend); + break; + case DIAMOND: + ip.drawLine(xbase,y,x,ybase); + ip.drawLine(x,ybase,xend,y); + ip.drawLine(xend,y,x,yend); + ip.drawLine(x,yend,xbase,y); + break; + case DOT: + ip.drawDot(x, y); //uses current line width + break; + case CUSTOM: + if (plotObject.macroCode==null || frame==null) + break; + if (x=frame.x+frame.width || y>=frame.y+frame.height) + break; + ImagePlus imp = new ImagePlus("", ip); + WindowManager.setTempCurrentImage(imp); + StringBuilder sb = new StringBuilder(140+plotObject.macroCode.length()); + sb.append("x="); sb.append(x); + sb.append(";y="); sb.append(y); + sb.append(";setColor('"); + sb.append(Tools.c2hex(plotObject.color)); + sb.append("');s="); sb.append(sc(1)); + boolean drawingLegend = pointIndex < 0; + double xVal = 0; + double yVal = 0; + if (!drawingLegend) { + xVal = plotObject.xValues[pointIndex]; + yVal = plotObject.yValues[pointIndex]; + } + sb.append(";i="); sb.append(drawingLegend ? 0 : pointIndex); + sb.append(";xval=" + xVal); + sb.append(";yval=" + yVal); + sb.append(";"); + sb.append(plotObject.macroCode); + if (!drawingLegend ||!sb.toString().contains("d2s") ) {// a graphical symbol won't contain "d2s" .. + String rtn = IJ.runMacro(sb.toString());//.. so it can go to the legend + if ("[aborted]".equals(rtn)) + plotObject.macroCode = null; + } + WindowManager.setTempCurrentImage(null); + break; + default: // CIRCLE, CONNECTED_CIRCLES: 5x5 oval approximated by 5x5 square without corners + if (sc(size) < 5.01) { + ip.drawLine(x-1, y-2, x+1, y-2); + ip.drawLine(x-1, y+2, x+1, y+2); + ip.drawLine(x+2, y+1, x+2, y-1); + ip.drawLine(x-2, y+1, x-2, y-1); + } else { + int r = sc(0.5f*size-0.5f); + ip.drawOval(x-r, y-r, 2*r, 2*r); + } + break; + } + } + + /** Fill the area of the symbols for data points (except for shape=DOT) + * Note that ip.fill, ip.fillOval etc. can't be used here: they do not care about the clip rectangle */ + void fillShape(int shape, int x0, int y0, int size) { + if (shape == DIAMOND) size = (int)(size*1.21); + int r = sc(size/2)-1; + switch(shape) { + case BOX: + for (int dy=-r; dy<=r; dy++) + for (int dx=-r; dx<=r; dx++) + ip.drawDot(x0+dx, y0+dy); + break; + case TRIANGLE: + int ybase = y0 - r - sc(1); + int yend = y0 + r; + double halfWidth = sc(size/2)+sc(1)-1; + double hwStep = halfWidth/(yend-ybase+1); + for (int y=yend; y>=ybase; y--, halfWidth -= hwStep) { + int dx = (int)(Math.round(halfWidth)); + for (int x=x0-dx; x<=x0+dx; x++) + ip.drawDot(x,y); + } + break; + case DIAMOND: + ybase = y0 - r - sc(1); + yend = y0 + r; + halfWidth = sc(size/2)+sc(1)-1; + hwStep = halfWidth/(yend-ybase+1); + for (int y=yend; y>=ybase; y--) { + int dx = (int)(Math.round(halfWidth-(hwStep+1)*Math.abs(y-y0))); + for (int x=x0-dx; x<=x0+dx; x++) + ip.drawDot(x,y); + } + break; + case CIRCLE: case CONNECTED_CIRCLES: + int rsquare = (r+1)*(r+1); + for (int dy=-r; dy<=r; dy++) + for (int dx=-r; dx<=r; dx++) + if (dx*dx + dy*dy <= rsquare) + ip.drawDot(x0+dx, y0+dy); + break; + } + } + + /** Adds an arrow from position 1 to 2 given in pixels; 'size' is the length of the arrowhead + * @deprecated Use as a public method is not supported any more because it is incompatible with rescaling */ + @Deprecated + public void drawArrow(int x1, int y1, int x2, int y2, double size) { + double dx = x2 - x1; + double dy = y2 - y1; + double ra = Math.sqrt(dx * dx + dy * dy); + dx /= ra; + dy /= ra; + int x3 = (int) Math.round(x2 - dx * size); //arrow base + int y3 = (int) Math.round(y2 - dy * size); + double r = 0.3 * size; + int x4 = (int) Math.round(x3 + dy * r); + int y4 = (int) Math.round(y3 - dx * r); + int x5 = (int) Math.round(x3 - dy * r); + int y5 = (int) Math.round(y3 + dx * r); + ip.moveTo(x1, y1); ip.lineTo(x2, y2); + ip.moveTo(x4, y4); ip.lineTo(x2, y2); ip.lineTo(x5, y5); + } + + private void drawVerticalErrorBars(float[] x, float[] y, float[] e) { + int nPoints = Math.min(Math.min(x.length, y.length), e.length); + for (int i=0; i0))) continue; + int x0 = scaleX(x[i]); + int yPlus = scaleYWithOverflow(y[i] + e[i]); + int yMinus = scaleYWithOverflow(y[i] - e[i]); + ip.moveTo(x0,yMinus); + ip.lineTo(x0, yPlus); + } + } + + private void drawHorizontalErrorBars(float[] x, float[] y, float[] e) { + int nPoints = Math.min(Math.min(x.length, y.length), e.length); + float[] xpoints = new float[2]; + float[] ypoints = new float[2]; + for (int i=0; i0))) continue; + int y0 = scaleY(y[i]); + int xPlus = scaleXWithOverflow(x[i] + e[i]); + int xMinus = scaleXWithOverflow(x[i] - e[i]); + ip.moveTo(xMinus,y0); + ip.lineTo(xPlus, y0); + } + } + + /** Draw a polygon line; NaN values interrupt it. */ + void drawFloatPolyline(ImageProcessor ip, float[] x, float[] y, int n) { + if (x==null || x.length==0) return; + int x1, y1; + boolean isNaN1; + int x2 = scaleX(x[0]); + int y2 = scaleY(y[0]); + boolean isNaN2 = Float.isNaN(x[0]) || Float.isNaN(y[0]) || (logXAxis && x[0]<=0) || (logYAxis && y[0]<=0);; + for (int i=1; i= frame.x+frame.width && right >= frame.x+frame.width) continue; + if (left < frame.x) left = frame.x; + if (left >= frame.x+frame.width) left = frame.x+frame.width-1; + if (right < frame.x) right = frame.x; + if (right >= frame.x+frame.width) right = frame.x+frame.width-1; + if (left != right) { + for (int xi = Math.min(left,right); xi <= Math.max(left,right); xi++) { + int yi = (int)Math.round(y1 + (double)(y2 - y1)*(double)(xi - x1)/(double)(x2 - x1)); + /* double yMin = Math.min(yF[i-1], yF[i]); + double yMax = Math.max(yF[i-1], yF[i]); + if (y < yMin) y = yMin; // dont extrapolate (in case rounding to pixels falls outside [xi, xi+1] interval) + if (y > yMax) y = yMax;*/ + ip.drawLine(xi, y0, xi, yi); + } + } else { + ip.drawLine(left, y0, left, y2); + } + } + } + + /** Returns only indexed and sorted plot objects, if at least one label is indexed like "1__MyLabel" */ + Vector getIndexedPlotObjects(){ + boolean withIndex = false; + int len = allPlotObjects.size(); + String[] labels = new String[len]; + Vector indexedObjects = new Vector(); + for(int jj = 0; jj < len; jj++){ + PlotObject plotObject = allPlotObjects.get(jj); + labels[jj] = ""; + if (plotObject.type == PlotObject.XY_DATA && !plotObject.hasFlag(PlotObject.HIDDEN) && plotObject.label != null) { + String label = plotObject.label; + if(label.indexOf("__") >=0 && label.indexOf("__") <= 2){ + labels[jj]= plotObject.label; + withIndex = true; + } + } + } + int[] ranks = Tools.rank(labels); + for(int jj = 0; jj < len; jj++){ + if(labels[ranks[jj]] != ""){ + int index = ranks[jj]; + indexedObjects.add(allPlotObjects.get(index)); + } + } + if(!withIndex) + return null; + return indexedObjects; + } + + /** Draw the legend */ + void drawLegend(PlotObject legendObject, ImageProcessor ip) { + ip.setFont(scFont(legendObject.getFont())); + int nLabels = 0; + int maxStringWidth = 0; + float maxLineThickness = 0; + Vector usedPlotObjects = allPlotObjects; + Vector indexedObjects = getIndexedPlotObjects(); + if(indexedObjects != null) + usedPlotObjects= indexedObjects; + + for (PlotObject plotObject : usedPlotObjects) + if (plotObject.type == PlotObject.XY_DATA && !plotObject.hasFlag(PlotObject.HIDDEN) && plotObject.label != null) { //label exists: was set now or previously + nLabels++; + String label = plotObject.label; + if (indexedObjects != null) + label = label.substring(label.indexOf("__") + 2); + int w = ip.getStringWidth(label); + if (w > maxStringWidth) maxStringWidth = w; + if (plotObject.lineWidth > maxLineThickness) maxLineThickness = plotObject.lineWidth; + } + if (nLabels == 0) return; + if (pp.antialiasedText && scale > 1) //fix incorrect width of large fonts + maxStringWidth = (int)((1 + 0.004*scale) * maxStringWidth); + int frameThickness = sc(legendObject.lineWidth > 0 ? legendObject.lineWidth : 1); + FontMetrics fm = ip.getFontMetrics(); + ip.setJustification(LEFT); + int lineHeight = fm.getHeight(); + int height = nLabels*lineHeight + 2*sc(LEGEND_PADDING); + int width = maxStringWidth + sc(3*LEGEND_PADDING + LEGEND_LINELENGTH + maxLineThickness); + int positionCode = legendObject.flags & LEGEND_POSITION_MASK; + if (positionCode == AUTO_POSITION) + positionCode = autoLegendPosition(width, height, frameThickness); + Rectangle rect = legendRect(positionCode, width, height, frameThickness); + int x0 = rect.x; + int y0 = rect.y; + + ip.setColor(Color.white); + ip.setLineWidth(1); + if (!legendObject.hasFlag(LEGEND_TRANSPARENT)) { + ip.setRoi(x0, y0, width, height); + ip.fill(); + } else if (hasFlag(X_GRID | Y_GRID)) { //erase grid + int grid = ip instanceof ColorProcessor ? (gridColor.getRGB() & 0xffffff) : ip.getBestIndex(gridColor); + for (int y=y0; y=0) + label = label.substring(start+2); + } + ip.drawString(label, xText, y+ lineHeight/2); + y += bottomUp ? -lineHeight : lineHeight; + } + } + + /** The legend area; positionCode should be TOP_LEFT, TOP_RIGHT, etc. */ + Rectangle legendRect(int positionCode, int width, int height, int frameThickness) { + boolean leftPosition = positionCode == TOP_LEFT || positionCode == BOTTOM_LEFT; + boolean topPosition = positionCode == TOP_LEFT || positionCode == TOP_RIGHT; + int x0 = (leftPosition) ? + leftMargin + sc(2*LEGEND_PADDING) + frameThickness/2 : + leftMargin + frameWidth - width - sc(2*LEGEND_PADDING) - frameThickness/2; + int y0 = (topPosition) ? + topMargin + sc(LEGEND_PADDING) + frameThickness/2 : + topMargin + frameHeight - height - sc(LEGEND_PADDING) + frameThickness/2; + if (hasFlag(Y_TICKS)) + x0 += (leftPosition ? 1 : -1) * sc(tickLength - LEGEND_PADDING); + if (hasFlag(X_TICKS)) + y0 += (topPosition ? 1 : -1) * sc(tickLength - LEGEND_PADDING/2); + return new Rectangle(x0, y0, width, height); + } + + /** The position code of the legend position where the smallest amount of foreground pixels is covered */ + int autoLegendPosition(int width, int height, int frameThickness) { + int background = ip instanceof ColorProcessor ? (0xffffff) : (ip.isInvertedLut() ? 0 : 0xff); + int grid = ip instanceof ColorProcessor ? (gridColor.getRGB() & 0xffffff) : ip.getBestIndex(gridColor); + int bestPosition = 0; + int minCoveredPixels = Integer.MAX_VALUE; + for (int positionCode : new int[]{TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT}) { + Rectangle rect = legendRect(positionCode, width, height, frameThickness); + int coveredPixels = 0; + for (int y = rect.y - frameThickness/2; y <= rect.y + rect.height + frameThickness/2; y++) + for (int x = rect.x - frameThickness/2; x <= rect.x + rect.width + frameThickness/2; x++) { + int pixel = ip.getPixel(x, y) & 0xffffff; + if (pixel != background && pixel != grid) + coveredPixels ++; + } + if (coveredPixels < minCoveredPixels) { + minCoveredPixels = coveredPixels; + bestPosition = positionCode; + } + } + return bestPosition; + } + + /** Returns the x, y coordinates at the cursor position or the nearest point as a String */ + String getCoordinates(int x, int y) { + if (frame==null) return ""; + String text = ""; + if (!frame.contains(x, y)) + return text; + double xv = descaleX(x); // cursor location + double yv = descaleY(y); + boolean yIsValue = false; + if (!hasMultiplePlots()) { + PlotObject p = getMainCurveObject(); // display x and f(x) instead of cursor y + if (p != null) { + double bestDx = Double.MAX_VALUE; + double xBest = 0, yBest = 0; + for (int i=0; i=0; i--) { + if (allPlotObjects.get(i).type == PlotObject.XY_DATA) + return allPlotObjects.get(i); + } + return null; + } + + /** returns whether there are several plots so that one cannot give a single y value for a given x value */ + private boolean hasMultiplePlots() { + int nPlots = 0; + for (PlotObject plotObject : allPlotObjects) { + if (plotObject.type == PlotObject.ARROWS) + return true; + else if (plotObject.type == PlotObject.XY_DATA) { + nPlots ++; + if (nPlots > 1) return true; + } + } + return nPlots > 1; + } + + public void setPlotMaker(PlotMaker plotMaker) { + this.plotMaker = plotMaker; + } + + PlotMaker getPlotMaker() { + return plotMaker; + } + + /** Returns the labels of the (non-hidden) datasets as linefeed-delimited String. + * If the label is not set, a blank line is added. */ + String getDataLabels() { + String labels = ""; + boolean first = true; + for (PlotObject plotObject : allPlotObjects) + if (plotObject.type == PlotObject.XY_DATA && !plotObject.hasFlag(PlotObject.HIDDEN)) { + if (first) + first = false; + else + labels += '\n'; + if (plotObject.label != null) labels += plotObject.label; + } + return labels; + } + + /** Creates a ResultsTable with the plot data. Returns an empty table if no data. */ + public ResultsTable getResultsTable() { + return getResultsTable(true); + } + + /** Creates a ResultsTable with the data of the plot. Returns null if no data. + * Does not write the first x column if writeFirstXColumn is false. + * When all columns are the same length, x columns equal to the first x column are + * not written, independent of writeFirstXColumn. + * Column headings are "X", "Y", "X1", "Y1", etc, irrespective of any labels of the data sets + */ + public ResultsTable getResultsTable(boolean writeFirstXColumn) { + return getResultsTable(writeFirstXColumn, false); + } + + /** Creates a ResultsTable with the data of the plot. Returns null if no data. + * When all columns are the same length, x columns equal to the first x column are + * not written, independent of writeFirstXColumn. + * When the data sets have labels, they are used for column headings + */ + public ResultsTable getResultsTableWithLabels() { + return getResultsTable(true, true); + } + + /** Creates a ResultsTable with the data of the plot. Returns null if no data. + * Does not write the first x column if writeFirstXColumn is false. + * When all columns are the same length, x columns equal to the first x column are + * not written, independent of writeFirstXColumn. + * When the data sets have labels and useLabels is true, they are used for column headings, + * otherwise columns are named X, Y, X1, Y1, ... */ + ResultsTable getResultsTable(boolean writeFirstXColumn, boolean useLabels) { + ResultsTable rt = new ResultsTable(); + // find the longest x-value data set and count the data sets + int nDataSets = 0; + int tableLength = 0; + for (PlotObject plotObject : allPlotObjects) + if (plotObject.xValues != null) { + nDataSets++; + tableLength = Math.max(tableLength, plotObject.xValues.length); + } + if (nDataSets == 0) + return null; + // enter columns one by one to lists of data and headings + ArrayList headings = new ArrayList(2*nDataSets); + ArrayList data = new ArrayList(2*nDataSets); + int dataSetNumber = 0; + int arrowsNumber = 0; + PlotObject firstXYobject = null; + boolean allSameLength = true; + for (PlotObject plotObject : allPlotObjects) { + if (plotObject.type==PlotObject.XY_DATA) { + if (firstXYobject != null && firstXYobject.xValues.length!=plotObject.xValues.length) { + allSameLength = false; + break; + } + if (firstXYobject==null) + firstXYobject = plotObject; + } + } + firstXYobject = null; + for (PlotObject plotObject : allPlotObjects) { + if (plotObject.type==PlotObject.XY_DATA) { + boolean sameX = firstXYobject!=null && Arrays.equals(firstXYobject.xValues, plotObject.xValues) && allSameLength; + boolean sameXY = sameX && Arrays.equals(firstXYobject.yValues, plotObject.yValues); //ignore duplicates (e.g. Markers plus Curve) + boolean writeX = firstXYobject==null ? writeFirstXColumn : !sameX; + addToLists(headings, data, plotObject, dataSetNumber, writeX, /*writeY=*/!sameXY, /*multipleSets=*/nDataSets>1, useLabels); + if (firstXYobject == null) + firstXYobject = plotObject; + dataSetNumber++; + } else if (plotObject.type==PlotObject.ARROWS) { + addToLists(headings, data, plotObject, arrowsNumber, /*writeX=*/true, /*writeY=*/true, /*multipleSets=*/nDataSets>1, /*useLabels=*/false); + arrowsNumber++; + } + } + // populate the ResultsTable + int nColumns = headings.size(); + for (int line=0; line headings, ArrayListdata, PlotObject plotObject, + int dataSetNumber, boolean writeX, boolean writeY, boolean multipleSets, boolean useLabels) { + String plotObjectLabel = useLabels ? replaceSpacesEtc(plotObject.label) : null; + if (writeX) { + String label = null; // column header for x column + if (plotObject.type!=PlotObject.ARROWS) { + String plotXLabel = getLabel('x'); + if (dataSetNumber==0 && plotXLabel!=null) { // use x axis label for 1st dataset if permitted + if (useLabels) + label = replaceSpacesEtc(plotXLabel); + else if (plotXLabel.startsWith(" ") && plotXLabel.endsWith(" ")) // legacy: always use axis label for 1st data if spaces at start&end + label = plotXLabel.substring(1,plotXLabel.length()-1); + } else if (plotObjectLabel != null && dataSetNumber>0) + label = "X_"+plotObjectLabel; // use "X_" + dataset label + if (label != null && headings.contains(label)) + label = null; // avoid duplicate labels (not possible in ResultsTable) + } + if (label == null) { // create default label if no specific label yet + label = plotObject.type == PlotObject.ARROWS ? "XStart" : "X"; + if (multipleSets) label += dataSetNumber; + } + headings.add(label); + data.add(plotObject.xValues); + } + if (writeY) { + String label = null;; // column header for y column + if (plotObject.type!=PlotObject.ARROWS) { + String plotYLabel = getLabel('y'); + if (dataSetNumber==0 && plotYLabel!=null) { + if (useLabels && plotObjectLabel == null) // use y axis label for 1st dataset if no data set label + label = replaceSpacesEtc(plotYLabel); + else if (plotYLabel.startsWith(" ") && plotYLabel.endsWith(" ")) // legacy: always use axis label for 1st data if spaces at start&end + label = plotYLabel.substring(1,plotYLabel.length()-1); + } + if (plotObjectLabel != null) + label = plotObjectLabel; + if (label != null && headings.contains(label)) + label = null; // avoid duplicate labels (not possible in ResultsTable) + } + if (label == null) { // create default label if no specific label yet + label = plotObject.type == PlotObject.ARROWS ? "YStart" : "Y"; + if (multipleSets) label += dataSetNumber; + } + headings.add(label); + data.add(plotObject.yValues); + } + if (plotObject.xEValues != null) { + String label = plotObject.type == PlotObject.ARROWS ? "XEnd" : "XERR"; + if (multipleSets) label += dataSetNumber; + headings.add(label); + data.add(plotObject.xEValues); + } + if (plotObject.yEValues != null) { + String label = plotObject.type == PlotObject.ARROWS ? "YEnd" : "ERR"; + if (multipleSets) label += dataSetNumber; + headings.add(label); + data.add(plotObject.yEValues); + } + } + + /** Convert a string to a label suitable for a ResultsTable without whitespace, quotes or commas, + * to avoid problems when saving and reading the table. Returns null if an empty string or null. */ + static String replaceSpacesEtc(String s) { + if (s == null) return null; + s = s.trim().replaceAll("[\\s,]", "_").replace("\"","''"); + if (s.length() == 0) return null; + return s; + } + + /** get the number of digits for writing a column to the results table or the clipboard */ + static int getPrecision(float[] values) { + int setDigits = Analyzer.getPrecision(); + int measurements = Analyzer.getMeasurements(); + boolean scientificNotation = (measurements&Measurements.SCIENTIFIC_NOTATION)!=0; + if (scientificNotation) { + if (setDigits max) max = values[i]; + } + } + if (allInteger) + return 0; + int digits = (max - min) > 0 ? getDigits(min, max, MIN_FLOAT_PRECISION*(max-min), 15, 0) : + getDigits(max, MIN_FLOAT_PRECISION*Math.abs(max), 15, 0); + if (setDigits>Math.abs(digits)) + digits = setDigits * (digits < 0 ? -1 : 1); //use scientific notation if needed + return digits; + } + + /** Whether a given flag 'what' is set */ + boolean hasFlag(int what) { + return (pp.axisFlags&what) != 0; + } + + /* Obsolete, replaced by add(shape,x,y). */ + public void addPoints(String dummy, float[] x, float[] y, int shape) { + addPoints(x, y, shape); + } + + /** Plots a histogram from an array using auto-binning. + * @param values array containing the population + * N.Vischer + */ + public void addHistogram(double[] values) { + addHistogram(values, 0, 0); + } + + /** Plots a histogram from an array using the specified bin width. + * @param values array containing the population + * @param binWidth set zero for auto-binning + * N.Vischer + */ + public void addHistogram(double[] values, double binWidth) { + addHistogram(values, binWidth, 0); + } + + /** Plots a histogram of the value distribution (bin counts) from an array + * @param values array containing the values for the population + * @param binWidth set zero for auto-binning + * @param binCenter any x value can be the center of a bin + * N.Vischer + */ + public void addHistogram(double[] values, double binWidth, double binCenter) { + int len = values.length; + double min = Double.POSITIVE_INFINITY; + double max = Double.NEGATIVE_INFINITY; + double[] cleanVals = new double[len]; + int count = 0; + double sum = 0, sum2 = 0; + for (int i = 0; i < len; i++) { + double val = values[i]; + if (!Double.isNaN(val)) { + cleanVals[count++] = val; + sum += val; + sum2 += val * val; + if (val < min) + min = val; + if (val > max) + max = val; + } + } + if (binWidth <= 0) {//autobinning + double stdDev = Math.sqrt(((count * sum2 - sum * sum) / count) / count);//not count - 1 + // use Scott's method (1979 Biometrika, 66:605-610) for optimal binning: 3.49*sd*N^-1/3 + binWidth = 3.49 * stdDev * (Math.pow(count, -1.0 / 3)); + + } + double modCenter = binCenter % binWidth; + double modMin = min % binWidth; + double diff = modMin - modCenter; + double firstBin = min-diff; + while(firstBin - binWidth * 0.499 > min) + firstBin -= binWidth; + int nBins = (int) ((max - firstBin)/binWidth); + double lastBin = firstBin + nBins * binWidth; + while(lastBin + binWidth * 0.499 < max) + lastBin += binWidth; + nBins = (int) Math.round((lastBin - firstBin)/binWidth) + 1; + if (nBins == 1) + nBins = 2; + if (nBins > 9999) { + IJ.error("max bins > 9999"); + return; + } + double[] histo = new double[nBins]; + double[] xValues = new double[nBins]; + for (int i = 0; i < nBins; i++) + xValues[i] = firstBin + i * binWidth; + for (int i = 0; i < count; i++) { + double val = cleanVals[i]; + double indexD = (val - firstBin) / binWidth; + int index = (int) Math.round(indexD); + if (index < 0 || index >= nBins) { + IJ.error("index out of range"); + return; + } else + histo[index]++; + } + add("bar", xValues, histo); + } + + /* Obsolete, replaced by add("error bars",errorBars). */ + public void addErrorBars(String dummy, float[] errorBars) { + addErrorBars(errorBars); + } + + /* Obsolete; replaced by setFont(). */ + public void changeFont(Font font) { + setFont(font); + } + +} + +/** This class contains the properties of the plot, such as size, format, range, etc, except for the data+format (plot contents). + * To enable reading serialized PlotObjects of plots created with previous versions of ImageJ, + * the variable names MUST NEVER be changed! Also any additions should be made after careful thought, + * since they have to be kept for all future versions. */ +class PlotProperties implements Cloneable, Serializable { + /** The serialVersionUID should not be modified, otherwise saved plots won't be readable any more */ + static final long serialVersionUID = 1L; + // + PlotObject frame = new PlotObject(Plot.DEFAULT_FRAME_LINE_WIDTH); //the frame, including background color and axis numbering + PlotObject xLabel = new PlotObject(PlotObject.AXIS_LABEL); //the x axis label (string & font) + PlotObject yLabel = new PlotObject(PlotObject.AXIS_LABEL); //the x axis label (string & font) + PlotObject legend; //the legend (if any) + int width = 0; //canvas width (note: when stored, this must fit the image) + int height = 0; + int axisFlags; //these define axis layout + double[] rangeMinMax; //currentMinMax when writing, sets defaultMinMax when reading + boolean antialiasedText = true; + boolean isFrozen; //modifications (size, range, contents) don't update the ImageProcessor + + /** Returns an array of all PlotObjects defined as PlotProperties. Note that some may be null */ + PlotObject[] getAllPlotObjects() { + return new PlotObject[]{frame, xLabel, xLabel, legend}; + } + + /** Returns the PlotObject for xLabel ('x'), yLabel('y'), frame ('f'; includes number font) or the legend ('l'). */ + PlotObject getPlotObject(char c) { + switch(c) { + case 'x': return xLabel; + case 'y': return yLabel; + case 'f': return frame; + case 'l': return legend; + default: return null; + } + } + + /** A shallow clone that does not duplicate arrays or objects */ + public PlotProperties clone() { + try { + return (PlotProperties)(super.clone()); + } catch (CloneNotSupportedException e) { + return null; + } + } + + /** A deep clone; it also duplicates arrays and pPlotObjects */ + public PlotProperties deepClone() { + PlotProperties pp2 = clone(); //shallow clone + if (frame != null) pp2.frame = frame.deepClone(); + if (xLabel != null) pp2.xLabel = xLabel.deepClone(); + if (yLabel != null) pp2.yLabel = yLabel.deepClone(); + if (legend != null) pp2.legend = legend.deepClone(); + if (rangeMinMax != null) pp2.rangeMinMax = rangeMinMax.clone(); + return pp2; + } + +} // class PlotProperties + +/** This class contains the data and properties for displaying a curve, a set of arrows, a line or a label in a plot, + * as well as the legend, axis labels, and frame (including background and fonts of axis numbering). + * Note that all properties such as lineWidths and Fonts have to be scaled up for high-resolution plots. + * This class allows serialization for writing into tiff files. + * To enable reading serialized PlotObjects of plots created with previous versions of ImageJ, + * the variable names MUST NEVER be changed! Also any additions should be made after careful thought, + * since they have to be kept for all future versions. */ +class PlotObject implements Cloneable, Serializable { + /** The serialVersionUID should not be modified, otherwise saved plots won't be readable any more */ + static final long serialVersionUID = 1L; + /** Constants for the type of objects. These are powers of two so one can use them as masks */ + public final static int XY_DATA = 1, ARROWS = 2, LINE = 4, NORMALIZED_LINE = 8, DOTTED_LINE = 16, + LABEL = 32, NORMALIZED_LABEL = 64, LEGEND = 128, AXIS_LABEL = 256, FRAME = 512, SHAPES = 1024; + /** mask for recovering font style from the flags */ + final static int FONT_STYLE_MASK = 0x0f; + /** flag for the data set passed with the constructor. Note that 0 to 0x0f are reserved for fonts modifiers, 0x010-0x800 are reserved for legend modifiers */ + public final static int CONSTRUCTOR_DATA = 0x1000; + /** flag for hiding a PlotObject */ + public final static int HIDDEN = 0x2000; + /** Type of the object; XY_DATA stands for curve or markers, can be also ARROWS ... SHAPES */ + public int type = XY_DATA; + /** bitwise combination of flags, or the position of a legend */ + public int flags; + /** Options, currently only for FRAME, see Plot.setOptions */ + public String options; + /** The x and y data arrays and the error bars (if non-null). These arrays also serve as x0, y0, x1, y1 + * arrays for plotting arrays of arrows */ + public float[] xValues, yValues, xEValues, yEValues; + /** For SHAPES: For boxplots with whiskers ('boxes'), elements of the ArrayList are float[6] with x and all 5 y values + * (for 'boxesx', y and all 5 x values), for 'rectangles', float[4] with x1, y1, x2, y2. */ + public ArrayList shapeData; + /** For SHAPES only, shape type & options. Currently implemented: 'boxes', 'boxesx' (box plots with whiskers), 'rectangles', 'redraw_grid' */ + public String shapeType; //e.g. "boxes width=20" + /** Type of the points, such as Plot.LINE, Plot.CROSS etc. (for type = XY_DATA) */ + public int shape; + /** The line width in pixels for 'normal' plots (for high-resolution plots, to be multiplied by a scale factor) */ + public float lineWidth; + /** The color of the object, must not be null */ + public Color color; + /** The secondary color (for filling closed symbols and for the line of CIRCLES_AND_LINE, may be null for unfilled/default */ + public Color color2; + /** Labels and lines: Position (NORMALIZED objects: relative units 0...1). */ + public double x, y; + /** Lines only: End position */ + public double xEnd, yEnd; + /** Dotted lines only: step */ + public int step; + /** A label for the y data of the curve, a text to draw, or the text of a legend (tab-delimited lines) */ + public String label; + /** Labels only: Justification can be Plot.LEFT, Plot.CENTER or Plot.RIGHT */ + public int justification; + /** Macro code for drawing symbols */ + public String macroCode; + /** Text objects (labels, legend, axis labels) only: the font; maybe null for default. This is not serialized (transient) */ + private transient Font font; + /** String for representation of the font family (for Serialization); may be null for default. Font style is in flags, font size in fontSize. */ + private String fontFamily; + /** Font size (for Serialization), for 'normal' plots (for high-resolution plots, to be multiplied by a scale factor) */ + private float fontSize; + + + /** Generic constructor */ + PlotObject(int type) { + this.type = type; + } + + /** Constructor for XY_DATA, i.e., a curve or set of points */ + PlotObject(float[] xValues, float[] yValues, float[] yErrorBars, int shape, float lineWidth, Color color, Color color2, String yLabel) { + this.type = XY_DATA; + this.xValues = xValues; + this.yValues = yValues; + this.yEValues = yErrorBars; + this.shape = shape; + this.lineWidth = lineWidth; + this.color = color; + this.color2 = color2; + this.label = yLabel; + if (shape==Plot.CUSTOM) + this.macroCode = yLabel; + } + + /** Constructor for a set of arrows */ + PlotObject(float[] x1, float[] y1, float[] x2, float[] y2, float lineWidth, Color color) { + this.type = ARROWS; + this.xValues = x1; + this.yValues = y1; + this.xEValues = x2; + this.yEValues = y2; + this.lineWidth = lineWidth; + this.color = color; + } + + /** Constructor for a set of shapes */ + PlotObject(String shapeType, ArrayList shapeData, float lineWidth, Color color, Color color2) { + this.type = SHAPES; + this.shapeData = shapeData; + this.shapeType = shapeType; + this.lineWidth = lineWidth; + this.color = color; + this.color2 = color2; + } + + /** Constructor for a line */ + PlotObject(double x, double y, double xEnd, double yEnd, float lineWidth, int step, Color color, int type) { + this.type = type; + this.x = x; + this.y = y; + this.xEnd = xEnd; + this.yEnd = yEnd; + this.lineWidth = lineWidth; + this.step = step; + this.color = color; + } + + /** Constructor for a label or NORMALIZED_LABEL */ + PlotObject(String label, double x, double y, int justification, Font font, Color color, int type) { + this.type = type; + this.label = label; + this.x = x; + this.y = y; + this.justification = justification; + setFont(font); + this.color = color; + } + + /** Constructor for the legend. flags is bitwise or of TOP_LEFT etc. and LEGEND_TRANSPARENT if desired. + * Note that the labels in the legend are those of the data plotObjects */ + PlotObject(float lineWidth, Font font, Color color, int flags) { + this.type = LEGEND; + this.lineWidth = lineWidth; + setFont(font); + this.color = color; + this.flags = flags; + } + + /** Constructor for the frame, including axis numbers. In the current version, the primary color (line color) is always black */ + PlotObject(float lineWidth) { + this.type = FRAME; + this.color = Color.black; + this.lineWidth = lineWidth; + } + + /** Whether a given flag 'what' is set */ + boolean hasFlag(int what) { + return (flags&what) != 0; + } + + /** Sets a given flag */ + void setFlag(int what) { + flags |= what; + } + + /** Unsets a given flag */ + void unsetFlag(int what) { + flags = flags & (~what); + } + + /** Whether an XY_DATA object has a curve to draw */ + boolean hasCurve() { + return type == XY_DATA && (shape == Plot.LINE || shape == Plot.CONNECTED_CIRCLES || shape == Plot.FILLED); + } + + /** Whether an XY_DATA object has markers to draw */ + boolean hasMarker() { + return type == XY_DATA && (shape == Plot.CIRCLE || shape == Plot.X || shape == Plot.BOX || shape == Plot.TRIANGLE + || shape == Plot.CROSS || shape == Plot.DIAMOND || shape == Plot.DOT || shape == Plot.CONNECTED_CIRCLES + || shape == Plot.CUSTOM); + } + + /** Whether an XY_DATA object has markers that can be filled */ + boolean hasFilledMarker() { + return type == XY_DATA && color2 != null && (shape == Plot.CIRCLE || shape == Plot.BOX || shape == Plot.TRIANGLE || + shape == Plot.DIAMOND || shape == Plot.CONNECTED_CIRCLES); + } + + /** Size of the markers for an XY_DATA object with markers */ + int getMarkerSize () { + return lineWidth<=1 ? 5 : 7; + } + + /** Sets the font. Also writes font properties for serialization. */ + void setFont(Font font) { + if (font == this.font) return; + this.font = font; + if (font == null) { + fontFamily = null; + } else { + fontFamily = font.getFamily(); + flags = (flags & ~FONT_STYLE_MASK) | font.getStyle(); + fontSize = font.getSize2D(); + } + } + + /** Returns the font, or null if none specified */ + Font getFont() { + if (font == null && fontFamily != null) //after recovery from serialization, create the font from its description + font = FontUtil.getFont(fontFamily, flags&FONT_STYLE_MASK, fontSize); + return font; + } + + /** Returns all data xValues, yValues, xEValues, yEValues as a float[][] array. Note that future versions may have more data. */ + float[][] getAllDataValues() { + return new float[][] {xValues, yValues, xEValues, yEValues}; + } + + /** A shallow clone that does not duplicate arrays or objects */ + public PlotObject clone() { + try { + return (PlotObject)(super.clone()); + } catch (CloneNotSupportedException e) { + return null; + } + } + + /** A deep clone, which duplicates arrays etc. + * Note that colors & font are not cloned; it is assumed that these wil not be modified but replaced, + * so the clone remains unaffected */ + public PlotObject deepClone() { + PlotObject po2 = clone(); + if (xValues != null) po2.xValues = xValues.clone(); + if (yValues != null) po2.yValues = yValues.clone(); + if (xEValues != null) po2.xEValues = xEValues.clone(); + if (yEValues != null) po2.yEValues = yEValues.clone(); + if (shapeData != null) po2.shapeData = cloneArrayList(shapeData); + return po2; + } + + /** A clone of an array list one level deeper than a shallow clone. + * The clone() method of the objects in the list must be accessible */ + private ArrayList cloneArrayList(ArrayList src) { + ArrayList dest = (ArrayList)(src.clone()); //shallow clone + Class[] noClasses = new Class[0]; + Object[] noObjects = new Object[0]; + for (int i=0; i 1) + super.zoomIn(sx, sy); + else + super.zoomOut(sx, sy); + return; + } + plot.zoom(sx, sy, zoomFactor); + } + + /** Implements the Image/Zoom/Original Scale command. + * Sets the original range of the x, y axes (unless the plot is frozen) */ + public void unzoom() { + if (plot == null || plot.isFrozen()) { + super.unzoom(); + return; + } + resetMagnification(); + plot.setLimitsToDefaults(true); + } + + /** Implements the Image/Zoom/View 100% command: Sets the original frame size as specified + * in Edit/Options/Plots (unless the plot is frozen) */ + public void zoom100Percent() { + if (plot == null || plot.isFrozen()) { + super.zoom100Percent(); + return; + } + resetMagnification(); + plot.setFrameSize(PlotWindow.plotWidth, PlotWindow.plotHeight); + } + + /** Resizes the plot (unless frozen) to fit the window */ + public void fitToWindow() { + if (plot == null || plot.isFrozen()) { + super.fitToWindow(); + return; + } + ImageWindow win = imp.getWindow(); + if (win==null) return; + Rectangle bounds = win.getBounds(); + Dimension extraSize = win.getExtraSize(); + int width = bounds.width-extraSize.width;//(insets.left+insets.right+ImageWindow.HGAP*2); + int height = bounds.height-extraSize.height;//(insets.top+insets.bottom+ImageWindow.VGAP*2); + //IJ.log("fitToWindow "+bounds+"-> w*h="+width+"*"+height); + resizeCanvas(width, height); + getParent().doLayout(); + } + + /** Resizes the canvas when the user resizes the window. To avoid a race condition while creating + * a new window, this is ignored if no window exists or the window has not been activated yet. + */ + void resizeCanvas(int width, int height) { + if (plot == null || plot.isFrozen()) { + super.resizeCanvas(width, height); + return; + } + resetMagnification(); + if (width == oldWidth && height == oldHeight) return; + if (plot == null) return; + ImageWindow win = imp.getWindow(); + if (win==null || !(win instanceof PlotWindow)) return; + if (!win.isVisible()) return; + if (!((PlotWindow)win).wasActivated) return; // window layout not finished yet? + Dimension minSize = plot.getMinimumSize(); + int plotWidth = width < minSize.width ? minSize.width : width; + int plotHeight = height < minSize.height ? minSize.height : height; + plot.setSize(plotWidth, plotHeight); + setSize(width, height); + oldWidth = width; + oldHeight = height; + ((PlotWindow)win).canvasResized(); + } + + /** The image of a PlotCanvas is always shown at 100% magnification unless the plot is frozen */ + public void setMagnification(double magnification) { + if (plot == null || plot.isFrozen()) + super.setMagnification(magnification); + else + resetMagnification(); + } + + /** Scrolling a PlotCanvas is updating the plot, not viewing part of the plot, unless the plot is frozen */ + public void setSourceRect(Rectangle r) { + if (plot.isFrozen()) + super.setSourceRect(r); + else + resetMagnification(); + } + + void resetMagnification() { + magnification = 1.0; + srcRect.x = 0; + srcRect.y = 0; + } + + /** overrides ImageCanvas.setupScroll; if plot is not frozen, scrolling modifies the plot data range */ + protected void setupScroll(int ox, int oy) { + if (plot.isFrozen()) { + super.setupScroll(ox, oy); + return; + } + xMouseStart = ox; + yMouseStart = oy; + xScrolled = 0; + yScrolled = 0; + } + + /** overrides ImageCanvas.scroll; if plot is not frozen, scrolling modifies the plot data range */ + protected void scroll(int sx, int sy) { + if (plot.isFrozen()) { + super.scroll(sx, sy); + return; + } + if (sx == 0 && sy == 0) return; + if (xScrolled == 0 && yScrolled == 0) + plot.saveMinMax(); + int dx = sx - xMouseStart; + int dy = sy - yMouseStart; + plot.scroll(dx-xScrolled, dy-yScrolled); + xScrolled = dx; + yScrolled = dy; + Thread.yield(); + } + + /** overrides ImageCanvas.mouseExited; removes 'range' arrows */ + public void mouseExited(MouseEvent e) { + ImageWindow win = imp.getWindow(); + if (win instanceof PlotWindow) + ((PlotWindow)win).mouseExited(e); + super.mouseExited(e); + } + + /** overrides ImageCanvas.mousePressed: no further processing of clicks on 'range' arrows */ + public void mousePressed(MouseEvent e) { + rangeArrowIndexWhenPressed = getRangeArrowIndex(e); + if (rangeArrowIndexWhenPressed <0) + super.mousePressed(e); + } + + /** Overrides ImageCanvas.mouseReleased, handles clicks on 'range' arrows */ + public void mouseReleased(MouseEvent e) { + if (rangeArrowIndexWhenPressed>=0 && rangeArrowIndexWhenPressed==getRangeArrowIndex(e)) + plot.zoomOnRangeArrow(rangeArrowIndexWhenPressed); + else + super.mouseReleased(e); + } + + /** Returns the index of the arrow for modifying the range when the mouse click was + * at such an arrow, otherwise -1 */ + int getRangeArrowIndex(MouseEvent e) { + ImageWindow win = imp.getWindow(); + int rangeArrowIndex = -1; + if (win instanceof PlotWindow) { + int x = e.getX(); + int y = e.getY(); + rangeArrowIndex = ((PlotWindow)win).getRangeArrowIndex(x, y); + } + return rangeArrowIndex; + } +} diff --git a/src/ij/gui/PlotContentsDialog.java b/src/ij/gui/PlotContentsDialog.java new file mode 100644 index 0000000..c3ccdb0 --- /dev/null +++ b/src/ij/gui/PlotContentsDialog.java @@ -0,0 +1,671 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.measure.CurveFitter; +import ij.measure.Minimizer; +import ij.text.TextWindow; +import ij.measure.ResultsTable; +import ij.plugin.Colors; +import ij.plugin.frame.Recorder; +import ij.util.Tools; +import java.awt.*; +import java.awt.event.*; +import java.util.Arrays; +import java.util.Vector; +import java.util.ArrayList; + +/** This class implements the Plot Window's Data>"Add from Plot", "Add form Table", "Add Fit" and + * "More>Contents Style" dialogs + */ +public class PlotContentsDialog implements DialogListener { + /** Types of dialog (ERROR suppresses the dialog after an invalid call of a constructor) */ + public final static int ERROR=-1, STYLE=0, ADD_FROM_PLOT=1, ADD_FROM_TABLE=2, ADD_FROM_ARRAYS=3, ADD_FIT=4; + /** Dialog headings for each dialogType >= 0 */ + private static final String[] HEADINGS = new String[] {"Plot Contents Style", "Add From Plot", "Plot From Table", "Add Plot Data", "Add Fit"}; + private Plot plot; // the plot we work on + private int dialogType; // determines what to do: ADD_FORM_PLOT, etc. + private double[] savedLimits; // previous plot range, for undo upon cancel + GenericDialog gd; + private int currentObjectIndex = -1; + private Choice objectChoice; + private Choice symbolChoice; + private TextField colorField, color2Field, labelField, widthField; + private Checkbox visibleCheckbox; + private boolean creatingPlot; // for creating plots; dialogType determines source + private Choice plotChoice; // for "Add from Plot" + private Plot[] allPlots; + private String[] allPlotNames; + private static Plot previousPlot; + private static int previousPlotObjectIndex; + private int defaultPlotIndex, defaultObjectIndex; + private int currentPlotNumObjects; + private Choice tableChoice; // for "Add from Table" + final static int N_COLUMNS = 4; // number of data columns that we can have; x, y, xE, yE + private int nColumnsToUse = N_COLUMNS; // user may restrict to 2, for not having error bars + private Choice[] columnChoice = new Choice[N_COLUMNS]; + private final static String[] COLUMN_NAMES = new String[] {"X:", "Y:", "X Error:", "Y Error:"}; + private final static boolean[] COLUMN_ALLOW_NONE = new boolean[] {true, false, true, true}; //y data cannot be null + private ResultsTable[] allTables; + private String[] allTableNames; + private static String previousTableName; + private static int[] previousColumns = new int[]{1, 1, 0, 0}; //must be N_COLUMNS elements + private static int defaultTableIndex; + private static int[] defaultColumnIndex = new int[N_COLUMNS]; + private String[] arrayHeadings; // for "Add from Arrays" + private ArrayList arrayData; + private Choice fitDataChoice; // for "Add Fit" + private Choice fitFunctionChoice; + private Thread fittingThread; + private static String lastFitFunction = CurveFitter.fitList[0]; + private String curveFitterStatusString; + private static String previousColor="blue", previousColor2="#a0a0ff", previousSymbol="Circle"; + private static double previousLineWidth = 1; + private static final String[] PLOT_COLORS = new String[] {"blue", "red", "black", "#00c0ff", "#00e000", "gray", "#c08060", "magenta"}; + + + /** Prepares a new PlotContentsDialog for an existing plot. Use showDialog thereafter. + * @param dialogType may be STYLE (contents style), ADD_FROM_PLOT (add object from other plot), ADD_FROM_TABLE, and ADD_FIT */ + public PlotContentsDialog(Plot plot, int dialogType) { + this.plot = plot; + this.dialogType = plot == null ? ERROR : dialogType; + if (plot != null) currentPlotNumObjects = plot.getNumPlotObjects(); + } + + /** Prepares a new PlotContentsDialog for creating a new plot using data from a ResultsTable. + * Use showDialog thereafter. */ + public PlotContentsDialog(String title, ResultsTable rt) { + creatingPlot = true; + dialogType = ADD_FROM_TABLE; + if (rt == null || !isValid(rt)) { + IJ.error("Cant Create Plot","No (results) table or no data in "+title); + dialogType = ERROR; + } + plot = new Plot("Plot of "+title, "x", "y"); + allTables = new ResultsTable[] {rt}; + allTableNames = new String[] {title}; + } + + /** Prepares a new PlotContentsDialog for creating a new plot using float[] arrays as data. + * Each 'data' array in the ArrayList must have a corresponding element in the 'headings' array. + * 'defaultHeadings' may contain the headings of the items selected initially for x, y, x error and y error, respectively. + * 'defaultHeadings' and each of its entries may be null, and the array may have any length. */ + public PlotContentsDialog(String title, String[] headings, String[] defaultHeadings, ArrayList data) { + creatingPlot = true; + dialogType = ADD_FROM_ARRAYS; + this.arrayHeadings = headings; + this.arrayData = data; + plot = new Plot(title, "x", "y"); + setDefaultColumns(defaultHeadings); + } + + /** Prepares a new PlotContentsDialog for adding data from float[] arrays to a plot. + * Each 'data' array in the ArrayList must have a corresponding element in the 'headings' array. + * 'defaultHeadings' may contain the headings of the items selected initially for x, y, x error and y error, respectively. + * 'defaultHeadings' and each of its entries may be null, and the array may have any length. */ + public PlotContentsDialog(Plot plot, String[] headings, String[] defaultHeadings, ArrayList data) { + dialogType = ADD_FROM_ARRAYS; + this.plot = plot; + this.arrayHeadings = headings; + this.arrayData = data; + setDefaultColumns(defaultHeadings); + } + + /** Avoids showing a selection for the error bars; must be called before showDialog. */ + public void noErrorBars() { + nColumnsToUse = 2; // only x, y columns can be selected + } + + /** Shows the dialog, with a given parent Frame (may be null) */ + public void showDialog(Frame parent) { + if (dialogType == ERROR) return; + if (!creatingPlot) savedLimits = plot.getLimits(); + plot.savePlotObjects(); + String[] designations = plot.getPlotObjectDesignations(); + if (dialogType == STYLE && designations.length==0) { + IJ.error("Empty Plot"); + return; + } else if (dialogType == ADD_FROM_PLOT) { + prepareAddFromPlot(); + if (allPlots.length == 0) return; //should never happen; we have at least the current plot + } else if (dialogType == ADD_FROM_TABLE && !creatingPlot) { + prepareAddFromTable(); + if (allTables.length == 0) return; //should never happen; PlotWindow should not enable if no table + } + if ((dialogType == ADD_FROM_TABLE || dialogType == ADD_FROM_ARRAYS) && !creatingPlot) + suggestColor(); + if (parent == null && plot.getImagePlus() != null) + parent = plot.getImagePlus().getWindow(); + gd = parent == null ? new GenericDialog(HEADINGS[dialogType]) : + new GenericDialog(HEADINGS[dialogType], parent); + IJ.wait(100); //sometimes needed to avoid hanging? + if (dialogType == STYLE) { + gd.addChoice("Item:", designations, designations[0]); + objectChoice = (Choice)(gd.getChoices().get(0)); + currentObjectIndex = objectChoice.getSelectedIndex(); + } else if (dialogType == ADD_FROM_PLOT) { + gd.addChoice("Select Plot:", allPlotNames, allPlotNames[defaultPlotIndex]); + gd.addChoice("Item to Add:", new String[]{""}, ""); // will be set up by makeSourcePlotObjects + Vector choices = gd.getChoices(); + plotChoice = (Choice)(choices.get(0)); + objectChoice = (Choice)(choices.get(1)); + makeSourcePlotObjects(); + } else if (dialogType == ADD_FROM_TABLE) { + gd.addChoice("Select Table:", allTableNames, allTableNames[defaultTableIndex]); + tableChoice = (Choice)(gd.getChoices().get(0)); + if (creatingPlot) tableChoice.setEnabled(false); // we can't select the table, we have only one + } else if (dialogType == ADD_FIT) { + String[] dataSources = plot.getDataObjectDesignations(); + if (dataSources.length == 0) { + IJ.error("No Data For Fitting"); + return; + } + gd.addChoice("Fit Data Set:", dataSources, dataSources[0]); + gd.addChoice("Fit Function:", new String[0], ""); + Vector choices = gd.getChoices(); + fitDataChoice = (Choice)(choices.get(0)); + fitFunctionChoice = (Choice)(choices.get(1)); + if (dataSources.length == 1) + fitDataChoice.setEnabled(false); + for (int i=0; i 0) + Recorder.recordString("Plot.add(\"xerror\", Table.getColumn(\""+ + columnChoice[2].getSelectedItem()+"\", \""+tableChoice.getSelectedItem()+"\"));\n"); + if (columnChoice[3].getSelectedIndex() > 0) + Recorder.recordString("Plot.add(\"error\", Table.getColumn(\""+ + columnChoice[3].getSelectedItem()+"\", \""+tableChoice.getSelectedItem()+"\"));\n"); + } + Recorder.recordString("Plot.setStyle("+currentObjectIndex+", \""+getStyleString()+"\");\n"); + } + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (e==null) + return true; //gets called with e=null upon OK + boolean setStyle = false; + if (dialogType == STYLE) { + int objectIndex = objectChoice.getSelectedIndex(); // no getNextChoice since Choices depend on dialog type + setStyle = (e.getSource() != objectChoice); + if (e.getSource() == objectChoice) { + setDialogStyleFields(objectIndex); + currentObjectIndex = objectIndex; + } else + setStyle = true; + } else if (dialogType == ADD_FROM_PLOT) { + if (e.getSource() == plotChoice) { + makeSourcePlotObjects(); + addObjectFromPlot(); + } else if (e.getSource() == objectChoice) { + addObjectFromPlot(); + } else + setStyle = true; + } else if (dialogType == ADD_FROM_TABLE || dialogType == ADD_FROM_ARRAYS) { + if (e.getSource() == tableChoice) { + makeSourceColumns(); + addObjectFromTable(); + } else { + boolean columnChanged = false; + for (int c=0; c 0 ? label : null); + } + } + + /** Returns the style String for Plot.setPlotObjectStyle from the dialog fields */ + private String getStyleString() { + String color = colorField.getText(); + String color2 = color2Field.getText(); + double width = Tools.parseDouble(widthField.getText()); + String symbol = symbolChoice.getSelectedItem(); + Boolean visible = visibleCheckbox.getState(); + String style = color.trim()+","+color2.trim()+","+(float)width+","+ symbol+(visible?"":"hidden"); + return style; + } + + /** Sets the style fields of the dialog according to the style of the PlotObject having the + * index given. Does nothing with index < 0 */ + private void setDialogStyleFields(int index) { + if (index < 0) return; + Checkbox visibleC = (Checkbox)gd.getCheckboxes().get(0); + String styleString = plot.getPlotObjectStyle(index); + String designation = plot.getPlotObjectDesignations()[index].toLowerCase(); + boolean isData = designation.startsWith("data"); + boolean isText = designation.startsWith("text"); + boolean isBox = designation.startsWith("shapes") && + (designation.contains("boxes") || designation.contains("rectangles")); + boolean isGrid = designation.startsWith("shapes") && designation.contains("redraw_grid"); + + String[] items = styleString.split(","); + colorField.setText(items[0]); + color2Field.setText(items[1]); + widthField.setText(items[2]); + if (items.length >= 4) + symbolChoice.select(items[3]); + labelField.setText(isData ? plot.getPlotObjectLabel(index) : ""); + visibleC.setState(!styleString.contains("hidden")); + + colorField.setEnabled(!isGrid); //grid color is fixed + color2Field.setEnabled(isData || isBox);//only (some) data symbols and boxes have secondary (fill) color + widthField.setEnabled(!isText && !isGrid); //all non-Text types have line width + // visibleC.setEnabled(!isGrid); //allow to hide everything + symbolChoice.setEnabled(isData); //only data have a symbol to choose + labelField.setEnabled(isData); //only data have a label in the legend + } + + + /** Prepare the lists 'allPlots', 'allPlotNames' for the "Add from Plot" dialog. + * Also sets 'defaultPlotIndex', 'defaultObjectIndex' */ + private void prepareAddFromPlot() { + int[] windowIDlist = WindowManager.getIDList(); + ArrayList plotImps = new ArrayList(); + ImagePlus currentPlotImp = null; + for (int windowID : windowIDlist) { + ImagePlus imp = WindowManager.getImage(windowID); + if (imp == null || imp.getWindow() == null) continue; + Plot thePlot = (Plot)(imp.getProperty(Plot.PROPERTY_KEY)); + if (thePlot != null) { + if (thePlot == plot) + currentPlotImp = imp; + else + plotImps.add(imp); + } + } + if (currentPlotImp != null) + plotImps.add(currentPlotImp); // add current plot as the last one (usually not used) + if (plotImps.size() == 0) return; // should never happen; we have at least the current plot + + allPlots = new Plot[plotImps.size()]; + allPlotNames = new String[plotImps.size()]; + defaultPlotIndex = 0; + for (int i=0; i tableWindows = new ArrayList(); + Frame[] windows = WindowManager.getNonImageWindows(); + for (Frame win : windows) { + if (!(win instanceof TextWindow)) continue; + ResultsTable rt = ((TextWindow)win).getResultsTable(); + if (isValid(rt)) + tableWindows.add((TextWindow)win); + } + allTables = new ResultsTable[tableWindows.size()]; + allTableNames = new String[tableWindows.size()]; + defaultTableIndex = 0; + for (int i=0; i 0) { + String[] newHeadings = new String[columnHeadings.length - nBadColumns]; + int i=0; + for (String heading : columnHeadings) //copy 'good' headings + if (heading != null) + newHeadings[i++] = heading; + columnHeadings = newHeadings; + } + } else { // if (dialogType == ADD_FROM_ARRAYS) + columnHeadings = new String[arrayHeadings.length+1]; + System.arraycopy(arrayHeadings, 0, columnHeadings, 1, arrayHeadings.length); + } + columnHeadings[0] = "---"; + for (int c=0; c 0 && previousColumns[c] >= 0) + columnChoice[c].select(Math.min(columnChoice[c].getItemCount()-1, previousColumns[c])); + } + } + + /** For "Add from Table" and "Add from Arrays" adds item to the plot according to the current Choice settings + * and sets the Style fields for it. */ + private void addObjectFromTable() { + float[][] data = getDataArrays(); + if (data[1] == null) return; //no y data? then can't plot + String label = columnChoice[1].getSelectedItem(); //take label from y + int shape = Plot.toShape(symbolChoice.getSelectedItem()); + float lineWidth = (float)(Tools.parseDouble(widthField.getText())); + if (lineWidth > 0) + plot.setLineWidth(lineWidth); + plot.restorePlotObjects(); + if (savedLimits != null) + plot.setLimits(savedLimits); + plot.setColor(colorField.getText(), color2Field.getText()); + plot.addPoints(data[0], data[1], data[3], shape, label); + if (data[2] != null) + plot.addHorizontalErrorBars(data[2]); + if (creatingPlot) { + plot.setXYLabels(data[0]==null ? "x" : columnChoice[0].getSelectedItem(), columnChoice[1].getSelectedItem()); + plot.setLimitsToFit(false); + } else + plot.fitRangeToLastPlotObject(); + currentObjectIndex = plot.getNumPlotObjects()-1; + setDialogStyleFields(currentObjectIndex); + if (dialogType == ADD_FROM_TABLE) + previousTableName = allTableNames[tableChoice.getSelectedIndex()]; + } + + /** For "Add from Table" and "Add from Arrays", tries to set a 'y' data column that has not been plotted yet. */ + private void suggestNewYColumn() { + int nYcolumns = columnChoice[1].getItemCount(); + int currentIndex = columnChoice[1].getSelectedIndex(); + for (int i=0; i= 0) + data[c] = rt.getColumn(index); + previousColumns[c] = columnChoice[c].getSelectedIndex(); + } + } else { //if (dialogType == ADD_FROM_ARRAYS) + for (int c=0; c= 0) + data[c] = arrayData.get(index); + previousColumns[c] = columnChoice[c].getSelectedIndex(); + } + } + return data; + } + + /** Does the curve fit and adds the fit curve to the plot */ + private void addFitCurve() { + plot.restorePlotObjects(); + if (savedLimits != null) + plot.setLimits(savedLimits); + int dataIndex = fitDataChoice.getSelectedIndex(); + float[][] data = plot.getDataObjectArrays(dataIndex); + String fitName = fitFunctionChoice.getSelectedItem(); + int fitType = getIndex(CurveFitter.fitList, fitName); + CurveFitter cf = new CurveFitter(Tools.toDouble(data[0]), Tools.toDouble(data[1])); + cf.doFit(fitType); + String statusString = "Fit: "+Minimizer.STATUS_STRING[cf.getStatus()]; + if (cf.getStatus() == Minimizer.SUCCESS) + statusString += ", sum residuals ^2 = "+(float)(cf.getSumResidualsSqr()); + IJ.showStatus(statusString); + curveFitterStatusString = "Fit for "+plot.getTitle()+": "+fitDataChoice.getSelectedItem()+cf.getResultString(); //will be shown in Log when done + + double[] plotMinMax = plot.getLimits(); + double[] dataMinMax = Tools.getMinMax(data[0]); + double min = Math.min(plotMinMax[0], dataMinMax[0]); + double max = Math.max(plotMinMax[1], dataMinMax[1]); + double plotSpan = Math.abs(plotMinMax[1] - plotMinMax[0]); + double dataSpan = Math.abs(dataMinMax[1] - dataMinMax[0]); + double rangeFactor = Math.max(plotSpan/dataSpan, dataSpan/plotSpan); + if (rangeFactor > 20) rangeFactor = 20; + int nPoints = (int)(1000*rangeFactor); //finer data point spacing if we will want to zoom out or in + float[] xFit = new float[nPoints]; + float[] yFit = new float[nPoints]; + for (int i=0; i=3; + else + return columnHeadingStr.length() >= 1; + + } + + /** For "Add from Arrays", sets default columns from the colums titles */ + private void setDefaultColumns(String[] defaultHeadings) { + if (defaultHeadings == null) return; + for (int i=0; i 190 ? 255 : 255 - (int)(0.4*(255-v)); + } +} diff --git a/src/ij/gui/PlotDialog.java b/src/ij/gui/PlotDialog.java new file mode 100644 index 0000000..2f53362 --- /dev/null +++ b/src/ij/gui/PlotDialog.java @@ -0,0 +1,551 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.plugin.frame.Recorder; +import java.awt.*; +import java.util.Vector; + +/* + * This class contains dialogs for formatting of plots (range, axes, labels, legend, creating a high-resolution plot) + * Adding and formatting of contents (curves, symbols, ...) is in PlotContentsStyleDialog + */ + +public class PlotDialog implements DialogListener { + + /** Types of dialog. Note that 10-14 must be the same as the corresponding PlotWindow.rangeArrow numbers */ + public static final int SET_RANGE = 0, AXIS_OPTIONS = 1, LEGEND = 2, HI_RESOLUTION = 3, TEMPLATE = 4, //5-9 spare + X_LEFT = 10, X_RIGHT = 11, Y_BOTTOM = 12, Y_TOP = 13, X_AXIS = 14, Y_AXIS = 15; + /** Dialog headings for the dialogTypes */ + private static final String[] HEADINGS = new String[] {"Plot Range", "Axis Options", "Add Legend", "High-Resolution Plot", "Use Template", + null, null, null, null, null, // 5-9 spare + "X Left", "X Right", "Y Bottom","Y Top", "X Axis", "Y Axis"}; + /** Positions and corresponding codes for legend position */ + private static final String[] LEGEND_POSITIONS = new String[] {"Auto", "Top-Left", "Top-Right", "Bottom-Left", "Bottom-Right", "No Legend"}; + private static final int[] LEGEND_POSITION_N = new int[] {Plot.AUTO_POSITION, Plot.TOP_LEFT, Plot.TOP_RIGHT, Plot.BOTTOM_LEFT, Plot.BOTTOM_RIGHT, 0}; + /** Template "copy what" flag: dialog texts and corresponding bit masks, in the sequence as they appear in the dialog*/ + private static final String[] TEMPLATE_FLAG_NAMES = new String[] {"X Range", "Y Range", "Axis Style", "Labels", + "Legend", "Contents Style", "Extra Objects (Curves...)", "Window Size"}; + private static final int[] TEMPLATE_FLAGS = new int[] {Plot.X_RANGE, Plot.Y_RANGE, Plot.COPY_AXIS_STYLE, Plot.COPY_LABELS, + Plot.COPY_LEGEND, Plot.COPY_CONTENTS_STYLE, Plot.COPY_EXTRA_OBJECTS, Plot.COPY_SIZE}; + + private Plot plot; + private int dialogType; + private boolean minMaxSaved; //whether plot min&max has been saved for "previous range" + private boolean dialogShowing; //when the dialog is showing, ignore the last call with event null + private Plot[] templatePlots; + + private Checkbox xLogCheckbox, yLogCheckbox; + + //saved dialog options: legend + private static int legendPosNumber = 0; + private static boolean bottomUp; + private static boolean transparentBackground; + //saved dialog options: Axis labels + private static String lastXLabel, lastYLabel; + private static float plotFontSize; + //saved dialog options: High-resolution plot + private static float hiResFactor = 4.0f; + private static boolean hiResAntiAliased = true; + //saved dialog options: Use Template + private static int templateID; + private static int lastTemplateFlags = Plot.COPY_AXIS_STYLE|Plot.COPY_CONTENTS_STYLE; + + /** Constructs a new PlotDialog for a given plot and sets the type of dialog */ + public PlotDialog(Plot plot, int dialogType) { + this.plot = plot; + this.dialogType = dialogType; + } + + /** Asks the user for axis scaling; then replot with new scale on the same ImageProcessor. + * The 'parent' frame may be null */ + public void showDialog(Frame parent) { + if (dialogType == HI_RESOLUTION) { //'make high-resolution plot' dialog has no preview, handled separately + doHighResolutionDialog(parent); + return; + } + plot.savePlotPlotProperties(); + if (dialogType == TEMPLATE) + plot.savePlotObjects(); + + String dialogTitle = dialogType >= X_LEFT && dialogType <= Y_TOP ? + "Set Axis Limit..." : (HEADINGS[dialogType] + "..."); + GenericDialog gd = parent == null ? new GenericDialog(dialogTitle) : + new GenericDialog(dialogTitle, parent); + if (!setupDialog(gd)) return; + gd.addDialogListener(this); + dialogItemChanged(gd, null); //preview immediately + dialogShowing = true; + gd.showDialog(); + if (gd.wasCanceled()) { + plot.restorePlotProperties(); + if (dialogType == TEMPLATE) + plot.restorePlotObjects(); + plot.update(); + } else { + if (Recorder.record) + record(); + String xAxisLabel = plot.getLabel('x'); + if ((dialogType == AXIS_OPTIONS || dialogType == X_AXIS) && xAxisLabel != null && xAxisLabel.length() > 0) + lastXLabel = xAxisLabel; //remember for next time, in case we have none + String yAxisLabel = plot.getLabel('y'); + if ((dialogType == AXIS_OPTIONS || dialogType == Y_AXIS) && yAxisLabel != null && yAxisLabel.length() > 0) + lastYLabel = yAxisLabel; + if (dialogType == SET_RANGE || dialogType == X_AXIS || dialogType == Y_AXIS) + plot.makeLimitsDefault(); + if (dialogType == TEMPLATE) + lastTemplateFlags = plot.templateFlags; + + } + plot.killPlotPropertiesSnapshot(); + if (dialogType == TEMPLATE) + plot.killPlotObjectsSnapshot(); + + ImagePlus imp = plot.getImagePlus(); + ImageWindow win = imp == null ? null : imp.getWindow(); + if (win instanceof PlotWindow) + ((PlotWindow)win).hideRangeArrows(); // arrows etc might be still visible, but the mouse maybe elsewhere + + if (!gd.wasCanceled() && !gd.wasOKed()) { // user has pressed "Set all limits" or "Set Axis Options" button + int newDialogType = (dialogType == SET_RANGE) ? AXIS_OPTIONS : SET_RANGE; + new PlotDialog(plot, newDialogType).showDialog(parent); + } + } + + /** Setting up the dialog fields and initial parameters. The input is read in the dialogItemChanged method, which must + * have exactly the same structure of 'if' blocks and matching 'get' methods for each input field. + * @return false on error */ + private boolean setupDialog(GenericDialog gd) { + double[] currentMinMax = plot.getLimits(); + boolean livePlot = plot.plotMaker != null; + + int xDigits = plot.logXAxis ? -2 : Plot.getDigits(currentMinMax[0], currentMinMax[1], 0.005*Math.abs(currentMinMax[1]-currentMinMax[0]), 6, 0); + if (dialogType == SET_RANGE || dialogType == X_AXIS) { + gd.addNumericField("X_From", currentMinMax[0], xDigits, 6, "*"); + gd.addToSameRow(); + gd.addNumericField("To", currentMinMax[1], xDigits, 6, "*"); + gd.setInsets(0, 20, 0); //top, left, bottom + if (livePlot) + gd.addCheckbox("Fix_X Range While Live", (plot.templateFlags & Plot.X_RANGE) != 0); + gd.addCheckbox("Log_X Axis **", (plot.hasFlag(Plot.X_LOG_NUMBERS))); + xLogCheckbox = lastCheckboxAdded(gd); + enableDisableLogCheckbox(xLogCheckbox, currentMinMax[0], currentMinMax[1]); + } + int yDigits = plot.logYAxis ? -2 : Plot.getDigits(currentMinMax[2], currentMinMax[3], 0.005*Math.abs(currentMinMax[3]-currentMinMax[2]), 6, 0); + if (dialogType == SET_RANGE || dialogType == Y_AXIS) { + gd.setInsets(20, 0, 3); //top, left, bottom + gd.addNumericField("Y_From", currentMinMax[2], yDigits, 6, "*"); + gd.addToSameRow(); + gd.addNumericField("To", currentMinMax[3], yDigits, 6, "*"); + if (livePlot) + gd.addCheckbox("Fix_Y Range While Live", (plot.templateFlags & Plot.Y_RANGE) != 0); + gd.addCheckbox("Log_Y Axis **", (plot.hasFlag(Plot.Y_LOG_NUMBERS))); + yLogCheckbox = lastCheckboxAdded(gd); + enableDisableLogCheckbox(yLogCheckbox, currentMinMax[2], currentMinMax[3]); + } + if (dialogType >= X_LEFT && dialogType <= Y_TOP) { + int digits = dialogType < Y_BOTTOM ? xDigits : yDigits; + gd.addNumericField(HEADINGS[dialogType], currentMinMax[dialogType - X_LEFT], digits, 6, "*"); + } + + if (dialogType == AXIS_OPTIONS || dialogType == X_AXIS || dialogType == Y_AXIS) { + int flags = plot.getFlags(); + final String[] labels = new String[] {" Draw Grid", " Major Ticks", " Minor Ticks", " Ticks if Logarithmic", " Numbers"}; + final int[] xFlags = new int[] {Plot.X_GRID, Plot.X_TICKS, Plot.X_MINOR_TICKS, Plot.X_LOG_TICKS, Plot.X_NUMBERS}; + int rows = xFlags.length; + int columns = dialogType == AXIS_OPTIONS ? 2 : 1; + String[] allLabels = new String[rows*columns]; + boolean[] defaultValues = new boolean[rows*columns]; + String[] headings = dialogType == AXIS_OPTIONS ? new String[]{"X Axis", "Y Axis"} : null; + int i=0; + for (int l=0; l 80) nChars = 80; + //plotXLabel = plotXLabel.replace("\n", "|"); //multiline label currently no supported by Plot class + if (dialogType == AXIS_OPTIONS || dialogType == X_AXIS) + gd.addStringField("X Axis Label", plotXLabel, nChars); + //plotYLabel = plotYLabel.replace("\n", "|"); + if (dialogType == AXIS_OPTIONS || dialogType == Y_AXIS) + gd.addStringField("Y Axis Label", plotYLabel, nChars); + } + if (dialogType == SET_RANGE || dialogType == X_AXIS || dialogType == Y_AXIS) { + Font smallFont = IJ.font10; + gd.setInsets(10, 0, 0); //top, left, bottom + gd.addMessage("* Leave empty for automatic range", smallFont, Color.gray); + gd.setInsets(0, 0, 0); + gd.addMessage("** Requires limits > 0 and max/min > 3", smallFont, Color.gray); + if (dialogType == X_AXIS || dialogType == Y_AXIS) { + gd.setInsets(0, 0, 0); + gd.addMessage(" Label supports !!sub-!! and ^^superscript^^", smallFont, Color.gray); + } + } + + if (dialogType == AXIS_OPTIONS) { + Font plotFont = (plot.currentFont != null) ? plot.currentFont : plot.defaultFont; + Font labelFont = plot.getFont('x'); + if (labelFont == null) labelFont = plotFont; + Font numberFont = plot.getFont('f'); + if (numberFont == null) numberFont = plotFont; + gd.addNumericField("Number Font Size", numberFont.getSize2D(), 1); + gd.addNumericField("Label Font Size", labelFont.getSize2D(), 1); + //gd.setInsets(0, 20, 0); // no extra space + gd.addToSameRow(); + gd.addCheckbox("Bold", labelFont.isBold()); + } + if (dialogType == LEGEND) { + String labels = plot.getDataLabels(); + int nLines = labels.split("\n", -1).length; + Font legendFont = plot.getFont('l'); + if (legendFont == null) legendFont = (plot.currentFont != null) ? plot.currentFont : plot.defaultFont; + int lFlags = plot.getObjectFlags('l'); + if (lFlags != -1) { //if we have a legend already + for (int i=0; i= X_LEFT && dialogType <= Y_TOP) + gd.enableYesNoCancel("OK", "Set All Limits..."); + else if (dialogType == AXIS_OPTIONS) + gd.enableYesNoCancel("OK", "Set Range..."); + else if (dialogType == SET_RANGE) + gd.enableYesNoCancel("OK", "Set Axis Options..."); + return true; + } //setupDialog + + /** This method is called when the user changes something in the dialog. Note that the 'if's for reading + * the fields must be exactly the same as those for setting up the fields in 'setupDialog' (fields must be + * also read in the same sequence). */ + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (dialogShowing && e==null) + return true; //gets called with e=null upon OK; ignore this + boolean livePlot = plot.plotMaker != null; + + if (dialogType == SET_RANGE || dialogType == X_AXIS) { + double[] currentMinMax = plot.getLimits(); + double linXMin = gd.getNextNumber(); + //if (gd.invalidNumber()) + //linXMin = Double.NaN; + double linXMax = gd.getNextNumber(); + //if (gd.invalidNumber()) + //linXMax = Double.NaN; + if (linXMin == linXMax) return false; + if (!minMaxSaved) { + plot.saveMinMax(); //save for 'Previous Range' in plot menu + minMaxSaved = true; + } + if (livePlot) + plot.templateFlags = setFlag(plot.templateFlags, Plot.X_RANGE, gd.getNextBoolean()); + boolean xLog = gd.getNextBoolean(); + plot.setAxisXLog(xLog); + plot.setLimitsNoUpdate(linXMin, linXMax, currentMinMax[2], currentMinMax[3]); + currentMinMax = plot.getLimits(); + enableDisableLogCheckbox(xLogCheckbox, currentMinMax[0], currentMinMax[1]); + } + if (dialogType == SET_RANGE || dialogType == Y_AXIS) { + double[] currentMinMax = plot.getLimits(); + double linYMin = gd.getNextNumber(); + //if (gd.invalidNumber()) + //linYMin = Double.NaN; + double linYMax = gd.getNextNumber(); + //if (gd.invalidNumber()) + //linYMax = Double.NaN; + if (linYMin == linYMax) return false; + if (!minMaxSaved) { + plot.saveMinMax(); //save for 'Previous Range' in plot menu + minMaxSaved = true; + } + + if (livePlot) + plot.templateFlags = setFlag(plot.templateFlags, Plot.Y_RANGE, gd.getNextBoolean()); + boolean yLog = gd.getNextBoolean(); + plot.setAxisYLog(yLog); + plot.setLimitsNoUpdate(currentMinMax[0], currentMinMax[1], linYMin, linYMax); + currentMinMax = plot.getLimits(); + enableDisableLogCheckbox(yLogCheckbox, currentMinMax[2], currentMinMax[3]); + } + if (dialogType >= X_LEFT && dialogType <= Y_TOP) { + double newLimit = gd.getNextNumber(); + double[] minMaxCopy = (double[])(plot.getLimits().clone()); + minMaxCopy[dialogType - X_LEFT] = newLimit; + plot.setLimitsNoUpdate(minMaxCopy[0], minMaxCopy[1], minMaxCopy[2], minMaxCopy[3]); + } + + if (dialogType == AXIS_OPTIONS || dialogType == X_AXIS || dialogType == Y_AXIS) { + final int[] xFlags = new int[] {Plot.X_GRID, Plot.X_TICKS, Plot.X_MINOR_TICKS, Plot.X_LOG_TICKS, Plot.X_NUMBERS}; + int rows = xFlags.length; + int columns = dialogType == AXIS_OPTIONS ? 2 : 1; + int flags = 0; + if (dialogType == X_AXIS) + flags = plot.getFlags() & 0xaaaaaaaa; //keep y flags, i.e., odd bits + if (dialogType == Y_AXIS) + flags = plot.getFlags() & 0x55555555; //keep x flags, i.e., even bits + for (int l=0; l 24) numberFontSize = 24f; + float labelFontSize = (float)gd.getNextNumber(); + if (gd.invalidNumber()) labelFontSize = labelFont.getSize2D(); + boolean axisLabelBold = gd.getNextBoolean(); + plot.setFont('f', numberFont.deriveFont(numberFont.getStyle(), numberFontSize)); + plot.setAxisLabelFont(axisLabelBold ? Font.BOLD : Font.PLAIN, labelFontSize); + Font smallFont = new Font("SansSerif", Font.PLAIN, (int)(10*Prefs.getGuiScale())); + gd.addMessage("Labels support !!sub-!! and ^^superscript^^", smallFont, Color.gray); + } + if (dialogType == LEGEND) { + Font legendFont = plot.getFont('l'); + if (legendFont == null) legendFont = (plot.currentFont != null) ? plot.currentFont : plot.defaultFont; + + String labels = gd.getNextText(); + legendPosNumber = gd.getNextChoiceIndex(); + int lFlags = LEGEND_POSITION_N[legendPosNumber]; + float legendFontSize = (float)gd.getNextNumber(); + transparentBackground = gd.getNextBoolean(); + bottomUp = gd.getNextBoolean(); + if (bottomUp) + lFlags |= Plot.LEGEND_BOTTOM_UP; + if (transparentBackground) + lFlags |= Plot.LEGEND_TRANSPARENT; + plot.setColor(Color.black); + plot.setLineWidth(1); + plot.setLegend(labels, lFlags); + plot.setFont('l', legendFont.deriveFont(legendFont.getStyle(), legendFontSize)); + } + if (dialogType == TEMPLATE) { + Plot templatePlot = templatePlots[gd.getNextChoiceIndex()]; + ImagePlus imp = templatePlot.getImagePlus(); + if (imp != null) templateID = imp.getID(); //remember for next time + int templateFlags = 0; + for (int i=0; i0) //more range checking is done in Plot.setScale + hiResFactor = (float)scale; + hiResAntiAliased = !gd.getNextBoolean(); + final ImagePlus hiresImp = plot.makeHighResolution(title, hiResFactor, hiResAntiAliased, /*showIt=*/true); + /** The following command is needed to have the high-resolution plot as front window. Otherwise, as the + * dialog is owned by the original PlotWindow, the WindowManager will see the original plot as active, + * but the user interface will show the high-res plot as foreground window */ + EventQueue.invokeLater(new Runnable() {public void run() {IJ.selectWindow(hiresImp.getID());}}); + + if (Recorder.record) { + if (Recorder.scriptMode()) { + Recorder.recordCall("plot.makeHighResolution(\""+title+"\","+hiResFactor+","+hiResAntiAliased+",true);"); + } else { + String options = !hiResAntiAliased ? "disable" : ""; + if (options.length() > 0) + options = ",\""+options+"\""; + Recorder.recordString("Plot.makeHighResolution(\""+title+"\","+hiResFactor+options+");\n"); + } + } + } + + /** Disables switching on a checkbox for log range if the axis limits do not allow it. + * The checkbox can be always switched off. */ + void enableDisableLogCheckbox(Checkbox checkbox, double limit1, double limit2) { + boolean logPossible = limit1 > 0 && limit2 > 0 && (limit1 > 3*limit2 || limit2 > 3*limit1); + checkbox.setEnabled(logPossible); + } + + + boolean getFlag(int flags, int bitMask) { + return (flags&bitMask) != 0; + } + + int setFlag(int flags, int bitMask, boolean state) { + flags &= ~bitMask; + if (state) flags |= bitMask; + return flags; + } + + Checkbox lastCheckboxAdded(GenericDialog gd) { + Vector checkboxes = gd.getCheckboxes(); + return (Checkbox)(checkboxes.get(checkboxes.size() - 1)); + } + +} diff --git a/src/ij/gui/PlotMaker.java b/src/ij/gui/PlotMaker.java new file mode 100644 index 0000000..ddaef8a --- /dev/null +++ b/src/ij/gui/PlotMaker.java @@ -0,0 +1,15 @@ +package ij.gui; +import ij.ImagePlus; + +/** Plugins that generate "Live" profile plots (Profiler and ZAxisProfiler) + displayed in PlotWindows implement this interface. */ +public interface PlotMaker { + + /** Returns a profile plot. */ + public Plot getPlot(); + + /** Returns the ImagePlus used to generate the profile plots. */ + public ImagePlus getSourceImage(); + +} + diff --git a/src/ij/gui/PlotVirtualStack.java b/src/ij/gui/PlotVirtualStack.java new file mode 100644 index 0000000..6ef864a --- /dev/null +++ b/src/ij/gui/PlotVirtualStack.java @@ -0,0 +1,85 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import java.util.*; +import java.io.*; + +/** This is a virtual stack of frozen plots. */ +public class PlotVirtualStack extends VirtualStack { + private Vector plots = new Vector(50); + private int bitDepth = 8; + + public PlotVirtualStack(int width, int height) { + super(width, height); + this.bitDepth = bitDepth; + } + + /** Adds a plot to the end of the stack. */ + public void addPlot(Plot plot) { + plots.add(plot.toByteArray()); + if (plot.isColored()) + bitDepth = 24; + } + + /** Returns the pixel array for the specified slice, were 1<=n<=nslices. */ + public Object getPixels(int n) { + ImageProcessor ip = getProcessor(n); + if (ip!=null) + return ip.getPixels(); + else + return null; + } + + /** Returns an ImageProcessor for the specified slice, + were 1<=n<=nslices. Returns null if the stack is empty. */ + public ImageProcessor getProcessor(int n) { + byte[] bytes = (byte[])plots.get(n-1); + if (bytes!=null) { + try { + Plot plot = new Plot(null, new ByteArrayInputStream(bytes)); + ImageProcessor ip = plot.getProcessor(); + if (bitDepth==24) + ip = ip.convertToRGB(); + else if (bitDepth==8) + ip = ip.convertToByte(false); + return ip; + } catch (Exception e) { + IJ.handleException(e); + } + } + return null; + } + + /** Returns the number of slices in this stack. */ + public int getSize() { + return plots.size(); + } + + /** Returns either 24 (RGB) or 8 (grayscale). */ + public int getBitDepth() { + return bitDepth; + } + + public void setBitDepth(int bitDepth) { + this.bitDepth = bitDepth; + } + + public String getSliceLabel(int n) { + return null; + } + + public void setPixels(Object pixels, int n) { + } + + /** Deletes the specified slice, were 1<=n<=nslices. */ + public void deleteSlice(int n) { + if (n<1 || n>plots.size()) + throw new IllegalArgumentException("Argument out of range: "+n); + if (plots.size()<1) + return; + plots.remove(n-1); + } + + +} // PlotVirtualStack + diff --git a/src/ij/gui/PlotWindow.java b/src/ij/gui/PlotWindow.java new file mode 100644 index 0000000..b411f01 --- /dev/null +++ b/src/ij/gui/PlotWindow.java @@ -0,0 +1,917 @@ +package ij.gui; + +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.awt.datatransfer.*; +import java.util.*; +import ij.*; +import ij.process.*; +import ij.util.*; +import ij.text.TextWindow; +import ij.plugin.filter.Analyzer; +import ij.plugin.filter.PlugInFilterRunner; +import ij.measure.*; +import ij.io.SaveDialog; + +/** This class implements the Analyze/Plot Profile command. +* @author Michael Schmid +* @author Wayne Rasband +*/ +public class PlotWindow extends ImageWindow implements ActionListener, ItemListener, + ClipboardOwner, ImageListener, RoiListener, Runnable { + + private static final int WIDTH = 600; + private static final int HEIGHT = 340; + private static final int FONT_SIZE = 14; + private static final String PREFS_WIDTH = "pp.width"; + private static final String PREFS_HEIGHT = "pp.height"; + private static final String PREFS_FONT_SIZE = "pp.fontsize"; + + /** @deprecated */ + public static final int CIRCLE = Plot.CIRCLE; + /** @deprecated */ + public static final int X = Plot.X; + /** @deprecated */ + public static final int BOX = Plot.BOX; + /** @deprecated */ + public static final int TRIANGLE = Plot.TRIANGLE; + /** @deprecated */ + public static final int CROSS = Plot.CROSS; + /** @deprecated */ + public static final int LINE = Plot.LINE; + /** Write first X column when listing or saving. */ + public static boolean saveXValues = true; + /** Automatically close window after saving values. To set, use Edit/Options/Plots. */ + public static boolean autoClose; + /** Display the XY coordinates in a separate window. To set, use Edit/Options/Plots. */ + public static boolean listValues; + /** Interpolate line profiles. To set, use Edit/Options/Plots or setOption("InterpolateLines",boolean). */ + public static boolean interpolate = true; + // default values for new installations; values will be then saved in prefs + private static int defaultFontSize = Prefs.getInt(PREFS_FONT_SIZE, FONT_SIZE); + /** The width of the plot (without frame) in pixels. */ + public static int plotWidth = WIDTH; + /** The height of the plot in pixels. */ + public static int plotHeight = HEIGHT; + /** The plot text size, can be overridden by Plot.setFont, Plot.setFontSize, Plot.setXLabelFont etc. */ + public static int fontSize = defaultFontSize; + /** Have axes with no grid lines. If both noGridLines and noTicks are true, + * only min&max value of the axes are given */ + public static boolean noGridLines; + /** Have axes with no ticks. If both noGridLines and noTicks are true, + * only min&max value of the axes are given */ + public static boolean noTicks; + + private static final String OPTIONS = "pp.options"; + private static final int SAVE_X_VALUES = 1; + private static final int AUTO_CLOSE = 2; + private static final int LIST_VALUES = 4; + private static final int INTERPOLATE = 8; + private static final int NO_GRID_LINES = 16; + private static final int NO_TICKS = 32; + private static String moreButtonLabel = "More "+'\u00bb'; + private static String dataButtonLabel = "Data "+'\u00bb'; + + boolean wasActivated; // true after window has been activated once, needed by PlotCanvas + + private Button list, data, more, live; + private PopupMenu dataPopupMenu, morePopupMenu; + private static final int NUM_MENU_ITEMS = 20; //how many menu items we have in total + private MenuItem[] menuItems = new MenuItem[NUM_MENU_ITEMS]; + private Label statusLabel; + private String userStatusText; + private static String defaultDirectory = null; + private static int options; + private int defaultDigits = -1; + private int markSize = 5; + private static Plot staticPlot; + private Plot plot; + + private PlotMaker plotMaker; + private ImagePlus srcImp; // the source image for live plotting + private Thread bgThread; // thread for plotting (in the background) + private boolean doUpdate; // tells the background thread to update + + private Roi[] rangeArrowRois; // the overlays (arrows etc) for changing the range. Note: #10-15 must correspond to PlotDialog.dialogType! + private boolean rangeArrowsVisible; + private int activeRangeArrow = -1; + private static Color inactiveRangeArrowColor = Color.GRAY; + private static Color inactiveRangeRectColor = new Color(0x20404040, true); //transparent gray + private static Color activeRangeArrowColor = Color.RED; + private static Color activeRangeRectColor = new Color(0x18ff0000, true); //transparent red + + // static initializer + static { + options = Prefs.getInt(OPTIONS, SAVE_X_VALUES); + autoClose = (options&AUTO_CLOSE)!=0; + plotWidth = Prefs.getInt(PREFS_WIDTH, WIDTH); + plotHeight = Prefs.getInt(PREFS_HEIGHT, HEIGHT); + Dimension screen = IJ.getScreenSize(); + if (plotWidth>screen.width && plotHeight>screen.height) { + plotWidth = WIDTH; + plotHeight = HEIGHT; + } + } + + /** + * @deprecated + * replaced by the Plot class. + */ + public PlotWindow(String title, String xLabel, String yLabel, float[] xValues, float[] yValues) { + super(createImage(title, xLabel, yLabel, xValues, yValues)); + plot = staticPlot; + ((PlotCanvas)getCanvas()).setPlot(plot); + } + + /** + * @deprecated + * replaced by the Plot class. + */ + public PlotWindow(String title, String xLabel, String yLabel, double[] xValues, double[] yValues) { + this(title, xLabel, yLabel, Tools.toFloat(xValues), Tools.toFloat(yValues)); + } + + /** Creates a PlotWindow from a given ImagePlus with a Plot object. + * (called when reading an ImagePlus with an associated plot from a file) */ + public PlotWindow(ImagePlus imp, Plot plot) { + super(imp); + ((PlotCanvas)getCanvas()).setPlot(plot); + this.plot = plot; + draw(); + } + + /** Creates a PlotWindow from a Plot object. */ + PlotWindow(Plot plot) { + super(plot.getImagePlus()); + ((PlotCanvas)getCanvas()).setPlot(plot); + this.plot = plot; + draw(); + } + + /** Called by the constructor to generate the image the plot will be drawn on. + This is a static method because constructors cannot call instance methods. */ + static ImagePlus createImage(String title, String xLabel, String yLabel, float[] xValues, float[] yValues) { + staticPlot = new Plot(title, xLabel, yLabel, xValues, yValues); + return new ImagePlus(title, staticPlot.getBlankProcessor()); + } + + /** Sets the x-axis and y-axis range. + * @deprecated use the corresponding method of the Plot class */ + public void setLimits(double xMin, double xMax, double yMin, double yMax) { + plot.setLimits(xMin, xMax, yMin, yMax); + } + + /** Adds a set of points to the plot or adds a curve if shape is set to LINE. + * Note that there are more options available by using the methods of the Plot class instead. + * @param x the x-coodinates + * @param y the y-coodinates + * @param shape Plot.CIRCLE, X, BOX, TRIANGLE, CROSS, LINE etc. + * @deprecated use the corresponding method of the Plot class */ + public void addPoints(float[] x, float[] y, int shape) { + plot.addPoints(x, y, shape); + } + + /** Adds a set of points to the plot using double arrays. + * Must be called before the plot is displayed. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void addPoints(double[] x, double[] y, int shape) { + addPoints(Tools.toFloat(x), Tools.toFloat(y), shape); + } + + /** Adds vertical error bars to the plot. + * Must be called before the plot is displayed. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void addErrorBars(float[] errorBars) { + plot.addErrorBars(errorBars); + } + + /** Draws a label. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void addLabel(double x, double y, String label) { + plot.addLabel(x, y, label); + } + + /** Changes the drawing color. The frame and labels are + * always drawn in black. + * Must be called before the plot is displayed. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void setColor(Color c) { + plot.setColor(c); + } + + /** Changes the line width. + * Must be called before the plot is displayed. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void setLineWidth(int lineWidth) { + plot.setLineWidth(lineWidth); + } + + /** Changes the font. + * Must be called before the plot is displayed. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void changeFont(Font font) { + plot.changeFont(font); + } + + /** Displays the plot. */ + public void draw() { + Panel bottomPanel = new Panel(); + int hgap = IJ.isMacOSX()?1:5; + + list = new Button(" List "); + list.addActionListener(this); + bottomPanel.add(list); + bottomPanel.setLayout(new FlowLayout(FlowLayout.RIGHT,hgap,0)); + data = new Button(dataButtonLabel); + data.addActionListener(this); + bottomPanel.add(data); + more = new Button(moreButtonLabel); + more.addActionListener(this); + bottomPanel.add(more); + if (plot!=null && plot.getPlotMaker()!=null) { + live = new Button("Live"); + live.addActionListener(this); + bottomPanel.add(live); + } + statusLabel = new Label(); + statusLabel.setFont(new Font("Monospaced", Font.PLAIN, 12)); + statusLabel.setBackground(new Color(220, 220, 220)); + bottomPanel.add(statusLabel); + add(bottomPanel); + data.add(getDataPopupMenu()); + more.add(getMorePopupMenu()); + plot.draw(); + LayoutManager lm = getLayout(); + if (lm instanceof ImageLayout) + ((ImageLayout)lm).ignoreNonImageWidths(true); //don't expand size to make the panel fit + GUI.scale(bottomPanel); + maximizeCoordinatesLabelWidth(); + pack(); + + ImageProcessor ip = plot.getProcessor(); + boolean ipIsColor = ip instanceof ColorProcessor; + boolean impIsColor = imp.getProcessor() instanceof ColorProcessor; + if (ipIsColor != impIsColor) + imp.setProcessor(null, ip); + else + imp.updateAndDraw(); + if (listValues) + showList(/*useLabels=*/false); + else + ic.requestFocus(); //have focus on the canvas, not the button, so that pressing the space bar allows panning + } + + /** Sets the Plot object shown in this PlotWindow. Does not update the window. */ + public void setPlot(Plot plot) { + this.plot = plot; + ((PlotCanvas)getCanvas()).setPlot(plot); + } + + /** Releases the resources used by this PlotWindow */ + public void dispose() { + if (plot!=null) + plot.dispose(); + disableLivePlot(); + plot = null; + plotMaker = null; + srcImp = null; + super.dispose(); + } + + /** Called when the window is activated (WindowListener) + * Window layout is finished at latest a few millisec after windowActivated, then the + * 'wasActivated' boolean is set to tell the ImageCanvas that resize events should + * lead to resizing the canvas (before, creating the layout can lead to resize events)*/ + public void windowActivated(WindowEvent e) { + super.windowActivated(e); + if (!wasActivated) { + new Thread(new Runnable() { + public void run() { + IJ.wait(50); //sometimes, window layout is done only a few millisec after windowActivated + wasActivated = true; + } + }).start(); + } + } + + /** Called when the canvas is resized */ + void canvasResized() { + if (plot == null) return; + /*Dimension d1 = getExtraSize(); + Dimension d2 = plot.getMinimumSize(); + setMinimumSize(new Dimension(d1.width + d2.width, d1.height + d2.height));*/ + maximizeCoordinatesLabelWidth(); + } + + /** Maximizes the width for the coordinate&status readout field and its parent bottomPanel */ + void maximizeCoordinatesLabelWidth() { + Insets insets = getInsets(); //by default, left & right insets are 0 anyhow + Component parent = statusLabel.getParent(); //the bottomPanel, has insets of 0 + if (!parent.isValid()) parent.validate(); + int cWidth = getWidth() - 2*HGAP - statusLabel.getX() - insets.left - insets.right; + int cHeight = statusLabel.getPreferredSize().height; + statusLabel.setPreferredSize(new Dimension(cWidth, cHeight)); + parent.setSize(getWidth() - 2*HGAP, parent.getHeight()); + } + + /** Shows the text in the coordinate&status readout field at the bottom. + * This text may get temporarily replaced for 'tooltips' (mouse over range arrows etc.). + * Call with a null argument to enable coordinate readout again. */ + public void showStatus(String text) { + userStatusText = text; + if (statusLabel != null) + statusLabel.setText(text == null ? "" : text); + } + + /** Names for popupMenu items. Update NUM_MENU_ITEMS at the top when adding new ones! */ + private static int SAVE=0, COPY=1, COPY_ALL=2, LIST_SIMPLE=3, ADD_FROM_TABLE=4, ADD_FROM_PLOT=5, ADD_FIT=6, //data menu + SET_RANGE=7, PREV_RANGE=8, RESET_RANGE=9, FIT_RANGE=10, //the rest is in the more menu + ZOOM_SELECTION=11, AXIS_OPTIONS=12, LEGEND=13, STYLE=14, TEMPLATE=15, RESET_PLOT=16, + FREEZE=17, HI_RESOLUTION=18, PROFILE_PLOT_OPTIONS=19; + //the following commands are disabled when the plot is frozen + private static int[] DISABLED_WHEN_FROZEN = new int[]{ADD_FROM_TABLE, ADD_FROM_PLOT, ADD_FIT, + SET_RANGE, PREV_RANGE, RESET_RANGE, FIT_RANGE, ZOOM_SELECTION, AXIS_OPTIONS, LEGEND, STYLE, RESET_PLOT}; + + /** Prepares and returns the popupMenu of the Data>> button */ + PopupMenu getDataPopupMenu() { + dataPopupMenu = new PopupMenu(); + GUI.scalePopupMenu(dataPopupMenu); + menuItems[SAVE] = addPopupItem(dataPopupMenu, "Save Data..."); + menuItems[COPY] = addPopupItem(dataPopupMenu, "Copy 1st Data Set"); + menuItems[COPY_ALL] = addPopupItem(dataPopupMenu, "Copy All Data"); + menuItems[LIST_SIMPLE] = addPopupItem(dataPopupMenu, "List (Simple Headings)"); + dataPopupMenu.addSeparator(); + menuItems[ADD_FROM_TABLE] = addPopupItem(dataPopupMenu, "Add from Table..."); + menuItems[ADD_FROM_PLOT] = addPopupItem(dataPopupMenu, "Add from Plot..."); + menuItems[ADD_FIT] = addPopupItem(dataPopupMenu, "Add Fit..."); + return dataPopupMenu; + } + + /** Prepares and returns the popupMenu of the More>> button */ + PopupMenu getMorePopupMenu() { + morePopupMenu = new PopupMenu(); + GUI.scalePopupMenu(morePopupMenu); + menuItems[SET_RANGE] = addPopupItem(morePopupMenu, "Set Range..."); + menuItems[PREV_RANGE] = addPopupItem(morePopupMenu, "Previous Range"); + menuItems[RESET_RANGE] = addPopupItem(morePopupMenu, "Reset Range"); + menuItems[FIT_RANGE] = addPopupItem(morePopupMenu, "Set Range to Fit All"); + menuItems[ZOOM_SELECTION] = addPopupItem(morePopupMenu, "Zoom to Selection"); + morePopupMenu.addSeparator(); + menuItems[AXIS_OPTIONS] = addPopupItem(morePopupMenu, "Axis Options..."); + menuItems[LEGEND] = addPopupItem(morePopupMenu, "Legend..."); + menuItems[STYLE] = addPopupItem(morePopupMenu, "Contents Style..."); + menuItems[TEMPLATE] = addPopupItem(morePopupMenu, "Use Template..."); + menuItems[RESET_PLOT] = addPopupItem(morePopupMenu, "Reset Format"); + menuItems[FREEZE] = addPopupItem(morePopupMenu, "Freeze Plot", true); + menuItems[HI_RESOLUTION] = addPopupItem(morePopupMenu, "High-Resolution Plot..."); + morePopupMenu.addSeparator(); + menuItems[PROFILE_PLOT_OPTIONS] = addPopupItem(morePopupMenu, "Plot Defaults..."); + return morePopupMenu; + } + + MenuItem addPopupItem(PopupMenu popupMenu, String s) { + return addPopupItem(popupMenu, s, false); + } + + MenuItem addPopupItem(PopupMenu popupMenu, String s, boolean isCheckboxItem) { + MenuItem mi = null; + if (isCheckboxItem) { + mi = new CheckboxMenuItem(s); + ((CheckboxMenuItem)mi).addItemListener(this); + } else { + mi = new MenuItem(s); + mi.addActionListener(this); + } + popupMenu.add(mi); + return mi; + } + + /** Called if user has activated a button or popup menu item */ + public void actionPerformed(ActionEvent e) { + try { + Object b = e.getSource(); + if (b==live) + toggleLiveProfiling(); + else if (b==list) + showList(/*useLabels=*/true); + else if (b==data) { + enableDisableMenuItems(); + dataPopupMenu.show((Component)b, 1, 1); + } else if (b==more) { + enableDisableMenuItems(); + morePopupMenu.show((Component)b, 1, 1); + } else if (b==menuItems[SAVE]) + saveAsText(); + else if (b==menuItems[COPY]) + copyToClipboard(false); + else if (b==menuItems[COPY_ALL]) + copyToClipboard(true); + else if (b==menuItems[LIST_SIMPLE]) + showList(/*useLabels=*/false); + else if (b==menuItems[ADD_FROM_TABLE]) + new PlotContentsDialog(plot, PlotContentsDialog.ADD_FROM_TABLE).showDialog(this); + else if (b==menuItems[ADD_FROM_PLOT]) + new PlotContentsDialog(plot, PlotContentsDialog.ADD_FROM_PLOT).showDialog(this); + else if (b==menuItems[ADD_FIT]) + new PlotContentsDialog(plot, PlotContentsDialog.ADD_FIT).showDialog(this); + else if (b==menuItems[ZOOM_SELECTION]) { + if (imp!=null && imp.getRoi()!=null && imp.getRoi().isArea()) + plot.zoomToRect(imp.getRoi().getBounds()); + } else if (b==menuItems[SET_RANGE]) + new PlotDialog(plot, PlotDialog.SET_RANGE).showDialog(this); + else if (b==menuItems[PREV_RANGE]) + plot.setPreviousMinMax(); + else if (b==menuItems[RESET_RANGE]) + plot.setLimitsToDefaults(true); + else if (b==menuItems[FIT_RANGE]) + plot.setLimitsToFit(true); + else if (b==menuItems[AXIS_OPTIONS]) + new PlotDialog(plot, PlotDialog.AXIS_OPTIONS).showDialog(this); + else if (b==menuItems[LEGEND]) + new PlotDialog(plot, PlotDialog.LEGEND).showDialog(this); + else if (b==menuItems[STYLE]) + new PlotContentsDialog(plot, PlotContentsDialog.STYLE).showDialog(this); + else if (b==menuItems[TEMPLATE]) + new PlotDialog(plot, PlotDialog.TEMPLATE).showDialog(this); + else if (b==menuItems[RESET_PLOT]) { + plot.setFont(Font.PLAIN, fontSize); + plot.setAxisLabelFont(Font.PLAIN, fontSize); + plot.setFormatFlags(Plot.getDefaultFlags()); + plot.setFrameSize(plotWidth, plotHeight); //updates the image only when size changed + plot.updateImage(); + } else if (b==menuItems[HI_RESOLUTION]) + new PlotDialog(plot, PlotDialog.HI_RESOLUTION).showDialog(this); + else if (b==menuItems[PROFILE_PLOT_OPTIONS]) + IJ.doCommand("Plots..."); + ic.requestFocus(); //have focus on the canvas, not the button, so that pressing the space bar allows panning + } catch (Exception ex) { IJ.handleException(ex); } + } + + private void enableDisableMenuItems() { + boolean frozen = plot.isFrozen(); //prepare menu according to 'frozen' state of plot + ((CheckboxMenuItem)menuItems[FREEZE]).setState(frozen); + for (int i : DISABLED_WHEN_FROZEN) + menuItems[i].setEnabled(!frozen); + if (!PlotContentsDialog.tableWindowExists()) + menuItems[ADD_FROM_TABLE].setEnabled(false); + if (plot.getDataObjectDesignations().length == 0) + menuItems[ADD_FIT].setEnabled(false); + } + + /** Called if the user activates/deactivates a CheckboxMenuItem */ + public void itemStateChanged(ItemEvent e) { + if (e.getSource()==menuItems[FREEZE]) { + boolean frozen = ((CheckboxMenuItem)menuItems[FREEZE]).getState(); + plot.setFrozen(frozen); + } + } + + /** + * Updates the X and Y values when the mouse is moved and, if appropriate, + * shows/hides the overlay with the triangular buttons for changing the axis + * range limits. + * Overrides mouseMoved() in ImageWindow. + * + * @see ij.gui.ImageWindow#mouseMoved + */ + public void mouseMoved(int x, int y) { + super.mouseMoved(x, y); + if (plot == null) + return; + String statusText = null; //coordinate readout, status or tooltip, will be shown in coordinate&status line + + //arrows and other symbols for modifying the plot range + if (x < plot.leftMargin || y > plot.topMargin + plot.frameHeight) { + if (!rangeArrowsVisible && !plot.isFrozen()) + showRangeArrows(); + if (activeRangeArrow < 0) //mouse is not on one of the symbols, ignore (nothing to display) + {} + else if (activeRangeArrow < 8) //mouse over an arrow: 0,3,4,7 for increase, 1,2,5,6 for decrease + statusText = ((activeRangeArrow+1)&0x02) != 0 ? "Decrease Range" : "Increase Range"; + else if (activeRangeArrow == 8) //it's the 'R' icon + statusText = "Reset Range"; + else if (activeRangeArrow == 9) //it's the 'F' icon + statusText = "Full Range (Fit All)"; + else if (activeRangeArrow >= 10 && + activeRangeArrow < 14) //space between arrow-pairs for single number + statusText = "Set limit..."; + else if (activeRangeArrow >= 14) + statusText = "Axis Range & Options..."; + boolean repaint = false; + if (activeRangeArrow >= 0 && !rangeArrowRois[activeRangeArrow].contains(x, y)) { + rangeArrowRois[activeRangeArrow].setFillColor( + activeRangeArrow < 10 ? inactiveRangeArrowColor : inactiveRangeRectColor); + repaint = true; //de-highlight arrow where cursor has moved out + activeRangeArrow = -1; + } + if (activeRangeArrow < 0) { //no currently highlighted arrow, do we have a new one? + int i = getRangeArrowIndex(x, y); + if (i >= 0) { //we have an arrow or symbol at cursor position + rangeArrowRois[i].setFillColor( + i < 14 ? activeRangeArrowColor : activeRangeRectColor); + activeRangeArrow = i; + repaint = true; + } + } + if (repaint) ic.repaint(); + } else if (rangeArrowsVisible) + hideRangeArrows(); + + if (statusText == null) + statusText = userStatusText != null ? userStatusText : plot.getCoordinates(x, y); + if (statusLabel != null) + statusLabel.setText(statusText); + } + + /** Called by PlotCanvas */ + void mouseExited(MouseEvent e) { + if (rangeArrowsVisible) + hideRangeArrows(); + } + + /** Mouse wheel: zooms when shift or ctrl is pressed, scrolls in x if space bar down, in y otherwise. */ + public synchronized void mouseWheelMoved(MouseWheelEvent e) { + if (plot.isFrozen() || !(ic instanceof PlotCanvas)) { //frozen plots are like normal images + super.mouseWheelMoved(e); + return; + } + int rotation = e.getWheelRotation(); + int amount = e.getScrollAmount(); + if (e.getX() < plot.leftMargin || e.getX() > plot.leftMargin + plot.frameWidth)//n__ + return; + if (e.getY() < plot.topMargin || e.getY() > plot.topMargin + plot.frameHeight) + return; + boolean ctrl = (e.getModifiers()&Event.CTRL_MASK)!=0; + if (amount<1) amount=1; + if (rotation==0) + return; + if (ctrl||IJ.shiftKeyDown()) { + double zoomFactor = rotation<0 ? Math.pow(2, 0.2) : Math.pow(0.5, 0.2); + Point loc = ic.getCursorLoc(); + int x = ic.screenX(loc.x); + int y = ic.screenY(loc.y); + ((PlotCanvas)ic).zoom(x, y, zoomFactor); + } else if (IJ.spaceBarDown()) + plot.scroll(rotation*amount*Math.max(ic.imageWidth/50, 1), 0); + else + plot.scroll(0, rotation*amount*Math.max(ic.imageHeight/50, 1)); + } + + /** + * Creates an overlay with triangular buttons and othr symbols for changing the axis range + * limits and shows it + */ + void showRangeArrows() { + if (imp == null) + return; + hideRangeArrows(); //in case we have old arrows from a different plot size or so + rangeArrowRois = new Roi[4 * 2 + 2 + 4 + 2]; //4 arrows per axis, + 'Reset' and 'Fit All' icons, + 4 numerical input boxes + 2 axes + int i = 0; + int height = imp.getHeight(); + int arrowH = plot.topMargin < 14 ? 6 : 8; //height of arrows and distance between them; base is twice that value + float[] yP = new float[]{height - arrowH / 2, height - 3 * arrowH / 2, height - 5 * arrowH / 2 - 0.1f}; + + for (float x : new float[]{plot.leftMargin, plot.leftMargin + plot.frameWidth}) { //create arrows for x axis + float[] x0 = new float[]{x - arrowH / 2, x - 3 * arrowH / 2 - 0.1f, x - arrowH / 2}; + rangeArrowRois[i++] = new PolygonRoi(x0, yP, 3, Roi.POLYGON); + float[] x1 = new float[]{x + arrowH / 2, x + 3 * arrowH / 2 + 0.1f, x + arrowH / 2}; + rangeArrowRois[i++] = new PolygonRoi(x1, yP, 3, Roi.POLYGON); + } + float[] xP = new float[]{arrowH / 2 - 0.1f, 3 * arrowH / 2, 5 * arrowH / 2 + 0.1f}; + for (float y : new float[]{plot.topMargin + plot.frameHeight, plot.topMargin}) { //create arrows for y axis + float[] y0 = new float[]{y + arrowH / 2, y + 3 * arrowH / 2 + 0.1f, y + arrowH / 2}; + rangeArrowRois[i++] = new PolygonRoi(xP, y0, 3, Roi.POLYGON); + float[] y1 = new float[]{y - arrowH / 2, y - 3 * arrowH / 2 - 0.1f, y - arrowH / 2}; + rangeArrowRois[i++] = new PolygonRoi(xP, y1, 3, Roi.POLYGON); + } + Font theFont = new Font("SansSerif", Font.BOLD, 13); + + TextRoi txtRoi = new TextRoi(1, height - 19, "\u2009R\u2009", theFont); //thin spaces to make roi slightly wider + rangeArrowRois[8] = txtRoi; + TextRoi txtRoi2 = new TextRoi(20, height - 19, "\u2009F\u2009", theFont); + rangeArrowRois[9] = txtRoi2; + + rangeArrowRois[10] = new Roi(plot.leftMargin - arrowH/2 + 1, height - 5 * arrowH / 2, arrowH - 2, arrowH * 2);//numerical box left + rangeArrowRois[11] = new Roi(plot.leftMargin + plot.frameWidth - arrowH/2 + 1, height - 5 * arrowH / 2, arrowH - 2, arrowH * 2);//numerical box right + rangeArrowRois[12] = new Roi(arrowH / 2, plot.topMargin + plot.frameHeight - arrowH/2 + 1, arrowH * 2, arrowH -2);//numerical box bottom + rangeArrowRois[13] = new Roi(arrowH / 2, plot.topMargin - arrowH/2 + 1, arrowH * 2, arrowH - 2 );//numerical box top + + int topMargin = plot.topMargin; + int bottomMargin = topMargin + plot.frameHeight; + int leftMargin = plot.leftMargin; + int rightMargin = plot.leftMargin + plot.frameWidth; + rangeArrowRois[14] = new Roi(leftMargin, bottomMargin+2, // area to click for x axis options + rightMargin - leftMargin + 1, 2*arrowH); + rangeArrowRois[15] = new Roi(leftMargin-2*arrowH-2, topMargin, // area to click for y axis options + 2*arrowH, bottomMargin - topMargin + 1); + + Overlay ovly = imp.getOverlay(); + if (ovly == null) + ovly = new Overlay(); + for (Roi roi : rangeArrowRois) { + if (roi instanceof PolygonRoi) + roi.setFillColor(inactiveRangeArrowColor); + else if (roi instanceof TextRoi) { + roi.setStrokeColor(Color.WHITE); + roi.setFillColor(inactiveRangeArrowColor); + } else + roi.setFillColor(inactiveRangeRectColor); //transparent gray for single number boxes and axis range + ovly.add(roi); + } + imp.setOverlay(ovly); + ic.repaint(); + rangeArrowsVisible = true; + } + + void hideRangeArrows() { + if (imp == null || rangeArrowRois==null) return; + Overlay ovly = imp.getOverlay(); + if (ovly == null) return; + for (Roi roi : rangeArrowRois) + ovly.remove(roi); + ic.repaint(); + rangeArrowsVisible = false; + activeRangeArrow = -1; + } + + /** Returns the index of the range-modifying symbol or axis at the + * cursor position x,y, or -1 of none. + * Index numbers for arrows start with 0 at the 'down' arrow of the + * lower side of the x axis and end with 7 the up arrow at the upper + * side of the y axis. Numbers 8 & 9 are for "Reset Range" and "Fit All"; + * numbers 10-13 for a dialog to set a single limit, and 14-15 for the axis options. */ + + int getRangeArrowIndex(int x, int y) { + if (!rangeArrowsVisible) return -1; + for (int i=0; i0) { + plot.useTemplate(this.plot, this.plot.templateFlags | Plot.COPY_SIZE | Plot.COPY_LABELS | Plot.COPY_AXIS_STYLE | + Plot.COPY_CONTENTS_STYLE | Plot.COPY_LEGEND | Plot.COPY_EXTRA_OBJECTS); + plot.setPlotMaker(plotMaker); + this.plot = plot; + ((PlotCanvas)ic).setPlot(plot); + ImageProcessor ip = plot.getProcessor(); + if (ip!=null && imp!=null) { + imp.setProcessor(null, ip); + plot.setImagePlus(imp); + } + } + synchronized(this) { + if (doUpdate) { + doUpdate = false; //and loop again + } else { + try {wait();} //notify wakes up the thread + catch(InterruptedException e) { //interrupted tells the thread to exit + return; + } + } + } + } + } + + /** Returns the Plot associated with this PlotWindow. */ + public Plot getPlot() { + return plot; + } + + /** Freezes the active plot window, so the image does not get redrawn for zooming, + * setting the range, etc. */ + public static void freeze() { + Window win = WindowManager.getActiveWindow(); + if (win!=null && (win instanceof PlotWindow)) + ((PlotWindow)win).getPlot().setFrozen(true); + } + + public static void setDefaultFontSize(int size) { + if (size < 9) size = 9; + defaultFontSize = size; + } + + public static int getDefaultFontSize() { + return defaultFontSize; + } + +} diff --git a/src/ij/gui/PointRoi.java b/src/ij/gui/PointRoi.java new file mode 100644 index 0000000..2986b31 --- /dev/null +++ b/src/ij/gui/PointRoi.java @@ -0,0 +1,987 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.Colors; +import ij.plugin.PointToolOptions; +import ij.plugin.filter.Analyzer; +import ij.plugin.frame.Recorder; +import ij.util.Java2; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.KeyEvent; +import java.util.*; +import java.awt.geom.*; + +/** This class represents a collection of points that can be associated with counters. + * @see PointProperties.js +*/ +public class PointRoi extends PolygonRoi { + public static final String[] sizes = {"Tiny", "Small", "Medium", "Large", "Extra Large", "XXL", "XXXL"}; + public static final String[] types = {"Hybrid", "Cross", "Dot", "Circle"}; + public static final int HYBRID=0, CROSS=1, CROSSHAIR=1, DOT=2, CIRCLE=3; + private static final String TYPE_KEY = "point.type"; + private static final String SIZE_KEY = "point.size"; + private static final String CROSS_COLOR_KEY = "point.cross.color"; + private static final int TINY=1, SMALL=3, MEDIUM=5, LARGE=7, EXTRA_LARGE=11, XXL=17, XXXL=25; + private static final BasicStroke twoPixelsWide = new BasicStroke(2); + private static final BasicStroke threePixelsWide = new BasicStroke(3); + private static final BasicStroke fivePixelsWide = new BasicStroke(5); + private static int defaultType = HYBRID; + private static int defaultSize = SMALL; + private static Font font; + private static Color defaultCrossColor = Color.white; + private static int fontSize = 9; + public static final int MAX_COUNTERS = 100; + private static String[] counterChoices; + private static Color[] colors; + private boolean showLabels; + private int type = HYBRID; + private int size = SMALL; + private static int defaultCounter; + private int counter; + private int nCounters = 1; + private short[] counters; //for each point, 0-100 for counter (=category that can be defined by the user) + private int[] positions; //for each point, the stack slice, or 0 for 'show on all' + private int[] counts = new int[MAX_COUNTERS]; + private ResultsTable rt; + private long lastPointTime; + private int[] counterInfo; + private boolean promptBeforeDeleting; + private boolean promptBeforeDeletingCalled; + private int nMarkers; + private boolean addToOverlay; + public static PointRoi savedPoints; + + static { + setDefaultType((int)Prefs.get(TYPE_KEY, HYBRID)); + setDefaultSize((int)Prefs.get(SIZE_KEY, 1)); + } + + public PointRoi() { + this(0.0, 0.0); + deletePoint(0); + } + + /** Creates a new PointRoi using the specified int arrays of offscreen coordinates. */ + public PointRoi(int[] ox, int[] oy, int points) { + super(itof(ox), itof(oy), points, POINT); + width+=1; height+=1; + updateCounts(); + } + + /** Creates a new PointRoi using the specified float arrays of offscreen coordinates. */ + public PointRoi(float[] ox, float[] oy, int points) { + super(ox, oy, points, POINT); + width+=1; height+=1; + updateCounts(); + } + + /** Creates a new PointRoi using the specified float arrays of offscreen coordinates. */ + public PointRoi(float[] ox, float[] oy) { + this(ox, oy, ox.length); + } + + /** Creates a new PointRoi using the specified coordinate arrays and options. */ + public PointRoi(float[] ox, float[] oy, String options) { + this(ox, oy, ox.length); + setOptions(options); + } + + /** Creates a new PointRoi from a FloatPolygon. */ + public PointRoi(FloatPolygon poly) { + this(poly.xpoints, poly.ypoints, poly.npoints); + } + + /** Creates a new PointRoi from a Polygon. */ + public PointRoi(Polygon poly) { + this(itof(poly.xpoints), itof(poly.ypoints), poly.npoints); + } + + /** Creates a new PointRoi using the specified coordinates and options. */ + public PointRoi(double ox, double oy, String options) { + super(makeXorYArray(ox, null, false), makeXorYArray(oy, null, true), 1, POINT); + width=1; height=1; + incrementCounter(null); + setOptions(options); + } + + /** Creates a new PointRoi using the specified offscreen int coordinates. */ + public PointRoi(int ox, int oy) { + super(makeXorYArray(ox, null, false), makeXorYArray(oy, null, true), 1, POINT); + width=1; height=1; + incrementCounter(null); + } + + /** Creates a new PointRoi using the specified offscreen double coordinates. */ + public PointRoi(double ox, double oy) { + super(makeXorYArray(ox, null, false), makeXorYArray(oy, null, true), 1, POINT); + width=1; height=1; + incrementCounter(null); + } + + /** Creates a new PointRoi using the specified screen coordinates. */ + public PointRoi(int sx, int sy, ImagePlus imp) { + super(makeXorYArray(sx, imp, false), makeXorYArray(sy, imp, true), 1, POINT); + //defaultCounter = 0; + setImage(imp); + width=1; height=1; + type = defaultType; + size = defaultSize; + showLabels = !Prefs.noPointLabels; + if (imp!=null) { + int r = 10 + size; + double mag = ic!=null?ic.getMagnification():1; + if (mag<1) + r = (int)(r/mag); + imp.draw(x-r, y-r, 2*r, 2*r); + } + setCounter(Toolbar.getMultiPointMode()?defaultCounter:0); + incrementCounter(imp); + enlargeArrays(50); + if (Recorder.record) { + String add = Prefs.pointAddToOverlay?" add":""; + String options = sizes[convertSizeToIndex(size)]+" "+Colors.colorToString(getColor())+" "+types[type]+add; + options = options.toLowerCase(); + if (Recorder.scriptMode()) + Recorder.recordCall("imp.setRoi(new PointRoi("+x+","+y+",\""+options+"\"));"); + else + Recorder.record("makePoint", x, y, options); + } + } + + public void setOptions(String options) { + if (options==null) + return; + if (options.contains("tiny")) size=TINY; + else if (options.contains("medium")) size=MEDIUM; + else if (options.contains("extra")) size=EXTRA_LARGE; + else if (options.contains("large")) size=LARGE; + else if (options.contains("xxxl")) size=XXXL; + else if (options.contains("xxl")) size=XXL; + if (options.contains("cross")) type=CROSS; + else if (options.contains("dot")) type=DOT; + else if (options.contains("circle")) type=CIRCLE; + if (options.contains("nolabel")) setShowLabels(false); + else if (options.contains("label")) setShowLabels(true); + setStrokeColor(Colors.getColor(options,Roi.getColor())); + addToOverlay = options.contains("add"); + } + + static float[] itof(int[] arr) { + if (arr==null) + return null; + int n = arr.length; + float[] temp = new float[n]; + for (int i=0; i1) { + fontSize = 8; + double scale = size>=XXL?2:1.5; + fontSize += scale*convertSizeToIndex(size); + fontSize = (int)Math.round(fontSize); + //IJ.log("fontSize: "+fontSize+" "+scale); + font = new Font("SansSerif", Font.PLAIN, fontSize); + g.setFont(font); + if (fontSize>9) + Java2.setAntialiasedText(g, true); + } + int slice = imp!=null&&positions!=null&&imp.getStackSize()>1?imp.getCurrentSlice():0; + ImageCanvas ic = imp!=null?imp.getCanvas():null; + if (ic!=null && overlay && ic.getShowAllList()!=null && ic.getShowAllList().contains(this) && !Prefs.showAllSliceOnly) + slice = 0; // draw point irrespective of currently selected slice + if (Prefs.showAllPoints) + slice = 0; + //IJ.log("draw: "+positions+" "+imp.getCurrentSlice()); + for (int i=0; i1.0) { + saveXform = g2d.getTransform(); + g2d.translate(x, y); + g2d.scale(flattenScale, flattenScale); + x = y = 0; + } + Color color = strokeColor!=null?strokeColor:ROIColor; + if (!overlay && isActiveOverlayRoi()) { + if (color==Color.cyan) + color = Color.magenta; + else + color = Color.cyan; + } + if (nCounters>1 && counters!=null && n<=counters.length) + color = getColor(counters[n-1]); + if (type==HYBRID || type==CROSS) { + if (type==HYBRID) + g.setColor(Color.white); + else { + g.setColor(color); + colorSet = true; + } + if (size>XXL) + g2d.setStroke(fivePixelsWide); + else if (size>LARGE) + g2d.setStroke(threePixelsWide); + g.drawLine(x-(size+2), y, x+size+2, y); + g.drawLine(x, y-(size+2), x, y+size+2); + } + if (type!=CROSS && size>SMALL) + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + if (type==HYBRID || type==DOT) { + if (!colorSet) { + g.setColor(color); + colorSet = true; + } + if (size>LARGE) + g2d.setStroke(onePixelWide); + if (size>LARGE && type==DOT) + g.fillOval(x-size2, y-size2, size, size); + else if (size>LARGE && type==HYBRID) + g.fillRect(x-(size2-2), y-(size2-2), size-4, size-4); + else if (size>SMALL && type==HYBRID) + g.fillRect(x-(size2-1), y-(size2-1), size-2, size-2); + else + g.fillRect(x-size2, y-size2, size, size); + } + if (showLabels && nPoints>1) { + int xoffset = 2; + if (size==LARGE) xoffset=3; + if (size==EXTRA_LARGE) xoffset=4; + if (size==XXL) xoffset=5; + if (size==XXXL) xoffset=7; + int yoffset = xoffset; + if (size>=LARGE) yoffset=yoffset-1; + if (nCounters==1) { + if (!colorSet) + g.setColor(color); + g.drawString(""+n, x+xoffset, y+yoffset+fontSize); + } else if (counters!=null) { + g.setColor(getColor(counters[n-1])); + g.drawString(""+counters[n-1], x+xoffset, y+yoffset+fontSize); + } + } + if ((size>TINY||type==DOT) && (type==HYBRID||type==DOT)) { + g.setColor(Color.black); + if (size>LARGE && type==HYBRID) + g.drawOval(x-(size2-1), y-(size2-1), size-3, size-3); + else if (size>SMALL && type==HYBRID) + g.drawOval(x-size2, y-size2, size-1, size-1); + else + g.drawOval(x-(size2+1), y-(size2+1), size+1, size+1); + } + if (type==CIRCLE) { + int scaledSize = (int)Math.round(size+1); + g.setColor(color); + if (size>LARGE) + g2d.setStroke(twoPixelsWide); + g.drawOval(x-scaledSize/2, y-scaledSize/2, scaledSize, scaledSize); + } + if (saveXform!=null) + g2d.setTransform(saveXform); + } + + public void drawPixels(ImageProcessor ip) { + ip.setLineWidth(Analyzer.markWidth); + double x0 = bounds == null ? x : bounds.x; + double y0 = bounds == null ? y : bounds.y; + for (int i=0; i=0 && index<=nPoints && counters!=null) { + counts[counters[index]]--; + for (int i=index; i1; + if (counter!=0 || isStack || counters!=null) { + if (counters==null) { + counters = new short[nPoints*2]; + positions = new int[nPoints*2]; + } + counters[nPoints-1] = (short)counter; + if (imp!=null) + positions[nPoints-1] = imp.getStackSize()>1 ? imp.getCurrentSlice() : 0; + //if (positions[nPoints-1]==0 || positions[nPoints-1]==1 || counters[nPoints-1]==0) + // IJ.log("incrementCounter: "+nPoints+" "+" "+positions[nPoints-1]+" "+counters[nPoints-1]+" "+imp); + if (nPoints+1==counters.length) { + short[] temp = new short[counters.length*2]; + System.arraycopy(counters, 0, temp, 0, counters.length); + counters = temp; + int[] temp1 = new int[counters.length*2]; + System.arraycopy(positions, 0, temp1, 0, positions.length); + positions = temp1; + } + } + if (rt!=null && WindowManager.getFrame(getCountsTitle())!=null) + displayCounts(); + } + + /** Returns the index of the current counter. */ + public int getCounter() { + return counter; + } + + /** Returns the count associated with the specified counter index. + * @see #getLastCounter + * @see PointProperties.js + */ + public int getCount(int counter) { + if (counter==0 && counters==null) + return nPoints; + else + return counts[counter]; + } + + /** Returns the index of the last counter. */ + public int getLastCounter() { + return nCounters - 1; + } + + /** Returns the number of counters. */ + public int getNCounters() { + int n = 0; + for (int counter=0; counter0) n++; + } + return n; + } + + /** Returns the counter assocated with the specified point. */ + public int getCounter(int index) { + if (counters==null || index>=counters.length) + return 0; + else + return counters[index]; + } + + public void resetCounters() { + for (int i=0; iroi if keepContained is true (false). */ + PointRoi checkContained(Roi roi, boolean keepContained) { + if (!roi.isArea()) return null; + FloatPolygon points = getFloatPolygon(); + FloatPolygon points2 = new FloatPolygon(); + for (int i=0; i=0 && type=0 && type=0 && index=0 && sizenCounters-1 && nCounters8||getNCounters()>1) && imp!=null && imp.getWindow()!=null; + } + + public void promptBeforeDeleting(Boolean prompt) { + promptBeforeDeleting = prompt; + promptBeforeDeletingCalled = true; + } + + public static void setDefaultCounter(int counter) { + defaultCounter = counter; + } + + /** Returns an array containing for each point: + * The counter number (0-100) in the lower 8 bits, and the slice number + * (or 0, if the point appears on all slices) in the higher 24 bits. + * Used when writing a Roi to file (RoiEncoder) */ + public int[] getCounters() { + if (nPoints>65535) + return null; + int[] temp = new int[nPoints]; + if (counters!=null) { + for (int i=0; i>8; + //IJ.log(i+" cnt="+counter+" slice="+position); + this.counters[i] = (short)counter; + this.positions[i] = position; + if (counternCounters-1) + nCounters = counter + 1; + } + updateCounts(); + } + } + + /** Updates the counts for each category in 'counters' */ + public void updateCounts() { + Arrays.fill(counts, 0); + for (int i=0; i=counts.length) ? 0 : counters[i]] ++; + } + + /** Returns the stack slice of the point with the given index, or 0 if no slice defined for this point */ + public int getPointPosition(int index) { + if (positions!=null && index1 && positions!=null) { + int nChannels = 1; + int nSlices = 1; + int nFrames = 1; + boolean isHyperstack = true; + if (imp.isComposite() || imp.isHyperStack()) { + nChannels = imp.getNChannels(); + nSlices = imp.getNSlices(); + nFrames = imp.getNFrames(); + int nDimensions = 2; + if (nChannels>1) nDimensions++; + if (nSlices>1) nDimensions++; + if (nFrames>1) nDimensions++; + if (nDimensions==3) { + isHyperstack = false; + if (nChannels>1) + firstColumnHdr = "Channel"; + } else + firstColumnHdr = "Image"; + } + int firstSlice = Integer.MAX_VALUE; + for (int i=0; i0 && positions[i]0) { + for (int i=0; ilastSlice) + lastSlice = positions[i]; + } + } + if (firstSlice>0) { + for (int slice=firstSlice; slice<=lastSlice; slice++) { + rt.setValue(firstColumnHdr, row, slice); + if (isHyperstack) { + int[] position = imp.convertIndexToPosition(slice); + if (nChannels>1) + rt.setValue("Channel", row, position[0]); + if (nSlices>1) + rt.setValue("Slice", row, position[1]); + if (nFrames>1) + rt.setValue("Frame", row, position[2]); + } + for (int counter=0; counter1?imp.getCurrentSlice():0; + for (int i=0; i=sx2 && sx<=sx2+size && sy>=sy2 && sy<=sy2+size) { + handle = i; + break; + } + } + return handle; + } + + /** Returns the points as an array of Points. + * Wilhelm Burger: modified to use FloatPolygon for correct point positions. + */ + public Point[] getContainedPoints() { + FloatPolygon p = getFloatPolygon(); + Point[] points = new Point[p.npoints]; + for (int i=0; i iterator() { + return new Iterator() { + final Point[] pnts = getContainedPoints(); + final int n = pnts.length; + int next = (n == 0) ? 1 : 0; + @Override + public boolean hasNext() { + return next < n; + } + @Override + public Point next() { + if (next >= n) { + throw new NoSuchElementException(); + } + Point pnt = pnts[next]; + next = next + 1; + return pnt; + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + protected int getClosestPoint(double x, double y, FloatPolygon points) { + int index = -1; + double distance = Double.MAX_VALUE; + int slice = imp!=null&&positions!=null&&imp.getStackSize()>1?imp.getCurrentSlice():0; + if (Prefs.showAllPoints) + slice = 0; + for (int i=0; i=0; i--) { + if (!roi.contains(p.xpoints[i],p.ypoints[i])) { + points.deletePoint(i); + } + } + return points; + } + + @Override + public void copyAttributes(Roi roi2) { + super.copyAttributes(roi2); + if (roi2 instanceof PointRoi) { + PointRoi p2 = (PointRoi)roi2; + this.type = p2.type; + this.size = p2.size; + this.showLabels = p2.showLabels; + this.fontSize = p2.fontSize; + } + } + + public void setCounterInfo(int[] info) { + counterInfo = info; + } + + public int[] getCounterInfo() { + return counterInfo; + } + + public boolean addToOverlay() { + return addToOverlay; + } + + public String toString() { + if (nPoints>1) + return ("Roi[Points, count="+nPoints+"]"); + else + return ("Roi[Point, x="+x+", y="+y+"]"); + } + + /** @deprecated */ + public void setHideLabels(boolean hideLabels) { + this.showLabels = !hideLabels; + } + + /** @deprecated */ + public static void setDefaultMarkerSize(String size) { + } + + /** @deprecated */ + public static String getDefaultMarkerSize() { + return sizes[defaultSize]; + } + + /** Deprecated */ + public static void setDefaultCrossColor(Color color) { + } + + /** Deprecated */ + public static Color getDefaultCrossColor() { + return null; + } + +} diff --git a/src/ij/gui/PolygonRoi.java b/src/ij/gui/PolygonRoi.java new file mode 100644 index 0000000..0257ead --- /dev/null +++ b/src/ij/gui/PolygonRoi.java @@ -0,0 +1,1701 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.frame.*; +import ij.util.Tools; +import ij.util.FloatArray; +import java.awt.*; +import java.awt.image.*; +import java.awt.geom.*; +import java.awt.event.*; + +/** This class represents a polygon region of interest or polyline of interest. */ +public class PolygonRoi extends Roi { + + protected int maxPoints = 1000; // will be increased if necessary + protected int[] xp, yp; // image coordinates relative to origin of roi bounding box + protected float[] xpf, ypf; // or alternative sub-pixel coordinates + protected int[] xp2, yp2; // absolute screen coordinates + protected int nPoints; + protected float[] xSpline,ySpline; // relative image coordinates + protected int splinePoints = 200; + Rectangle clip; + + private double angle1, degrees=Double.NaN; + private int xClipMin, yClipMin, xClipMax, yClipMax; + private boolean userCreated; + private int boxSize = 8; + + long mouseUpTime = 0; + + /** Creates a new polygon or polyline ROI from x and y coordinate arrays. + Type must be Roi.POLYGON, Roi.FREEROI, Roi.TRACED_ROI, Roi.POLYLINE, Roi.FREELINE or Roi.ANGLE.*/ + public PolygonRoi(int[] xPoints, int[] yPoints, int nPoints, int type) { + super(0, 0, null); + init1(nPoints, type); + xp = xPoints; + yp = yPoints; + if (type!=TRACED_ROI) { + xp = new int[nPoints]; + yp = new int[nPoints]; + for (int i=0; i1 && isLine()) + updateWideLine(lineWidth); + finishPolygon(); + } + + /** Creates a new polygon or polyline ROI from a Polygon. Type must be Roi.POLYGON, + Roi.FREEROI, Roi.TRACED_ROI, Roi.POLYLINE, Roi.FREELINE or Roi.ANGLE.*/ + public PolygonRoi(Polygon p, int type) { + this(p.xpoints, p.ypoints, p.npoints, type); + } + + /** Creates a new polygon or polyline ROI from a FloatPolygon. Type must be Roi.POLYGON, + Roi.FREEROI, Roi.TRACED_ROI, Roi.POLYLINE, Roi.FREELINE or Roi.ANGLE.*/ + public PolygonRoi(FloatPolygon p, int type) { + this(p.xpoints, p.ypoints, p.npoints, type); + } + + /** @deprecated */ + public PolygonRoi(int[] xPoints, int[] yPoints, int nPoints, ImagePlus imp, int type) { + this(xPoints, yPoints, nPoints, type); + setImage(imp); + } + + /** Starts the process of creating a new user-generated polygon or polyline ROI. */ + public PolygonRoi(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + int tool = Toolbar.getToolId(); + switch (tool) { + case Toolbar.POLYGON: + type = POLYGON; + break; + case Toolbar.FREEROI: + type = FREEROI; + break; + case Toolbar.FREELINE: + type = FREELINE; + break; + case Toolbar.ANGLE: + type = ANGLE; + break; + default: + type = POLYLINE; + break; + } + if (magnificationForSubPixel()) + enableSubPixelResolution(); + previousSX = sx; + previousSY = sy; + x = offScreenX(sx); + y = offScreenY(sy); + startXD = offScreenXD(sx); + startYD = offScreenYD(sy); + if (subPixelResolution()) { + setLocation(startXD, startYD); + xpf = new float[maxPoints]; + ypf = new float[maxPoints]; + double xbase = getXBase(); + double ybase = getYBase(); + xpf[0] = (float)(startXD-xbase); + ypf[0] = (float)(startYD-ybase); + xpf[1] = xpf[0]; + ypf[1] = ypf[0]; + } else { + xp = new int[maxPoints]; + yp = new int[maxPoints]; + } + xp2 = new int[maxPoints]; + yp2 = new int[maxPoints]; + nPoints = 2; + width=1; + height=1; + clipX = x; + clipY = y; + clipWidth = 1; + clipHeight = 1; + state = CONSTRUCTING; + userCreated = true; + if (lineWidth>1 && isLine()) + updateWideLine(lineWidth); + boxSize = (int)(boxSize*Prefs.getGuiScale()); + } + + private void drawStartBox(Graphics g) { + if (type!=ANGLE) + g.drawRect(screenXD(startXD)-4, screenYD(startYD)-4, 8, 8); + } + + public void draw(Graphics g) { + updatePolygon(); + Color color = strokeColor!=null?strokeColor:ROIColor; + boolean hasHandles = xSpline!=null||type==POLYGON||type==POLYLINE||type==ANGLE; + boolean isActiveOverlayRoi = !overlay && isActiveOverlayRoi(); + if (isActiveOverlayRoi) { + if (color==Color.cyan) + color = Color.magenta; + else + color = Color.cyan; + } + boolean fill = false; + mag = getMagnification(); + if (fillColor!=null && !isLine() && state!=CONSTRUCTING) { + color = fillColor; + fill = true; + } + g.setColor(color); + Graphics2D g2d = (Graphics2D)g; + setRenderingHint(g2d); + if (stroke!=null && !isActiveOverlayRoi) + g2d.setStroke(getScaledStroke()); + if (xSpline!=null) { + if (type==POLYLINE || type==FREELINE) { + drawSpline(g, xSpline, ySpline, splinePoints, false, fill, isActiveOverlayRoi); + if (wideLine && !overlay) { + g2d.setStroke(onePixelWide); + g.setColor(getColor()); + drawSpline(g, xSpline, ySpline, splinePoints, false, fill, isActiveOverlayRoi); + } + } else + drawSpline(g, xSpline, ySpline, splinePoints, true, fill, isActiveOverlayRoi); + } else { + if (type==POLYLINE || type==FREELINE || type==ANGLE || state==CONSTRUCTING) { + g.drawPolyline(xp2, yp2, nPoints); + if (wideLine && !overlay) { + g2d.setStroke(onePixelWide); + g.setColor(getColor()); + g.drawPolyline(xp2, yp2, nPoints); + } + } else { + if (fill) { + if (isActiveOverlayRoi) { + g.setColor(Color.cyan); + g.drawPolygon(xp2, yp2, nPoints); + } else + g.fillPolygon(xp2, yp2, nPoints); + } else + g.drawPolygon(xp2, yp2, nPoints); + } + if (state==CONSTRUCTING && type!=FREEROI && type!=FREELINE) + drawStartBox(g); + } + if (hasHandles && clipboard==null && !overlay) { + if (activeHandle>0) + drawHandle(g, xp2[activeHandle-1], yp2[activeHandle-1]); + if (activeHandle1f) + ip.setLineWidth((int)Math.round(getStrokeWidth())); + double xbase = getXBase(); + double ybase = getYBase(); + if (xSpline!=null) { + ip.moveTo((int)Math.round(xbase+xSpline[0]), (int)Math.round(ybase+ySpline[0])); + for (int i=1; i Math.abs(dy)) + dy = 0; + else + dx = 0; + sx = previousSX + dx; + sy = previousSY + dy; + } + + // Do rubber banding + int tool = Toolbar.getToolId(); + if (!(tool==Toolbar.POLYGON || tool==Toolbar.POLYLINE || tool==Toolbar.ANGLE)) { + imp.deleteRoi(); + imp.draw(); + return; + } + if (IJ.altKeyDown()) + wipeBack(); + + drawRubberBand(sx, sy); + + // show status: length & angle + degrees = Double.NaN; + double len = -1; + if (nPoints>1) { + double x1, y1, x2, y2; + if (xpf!=null) { + x1 = xpf[nPoints-2]; + y1 = ypf[nPoints-2]; + x2 = xpf[nPoints-1]; + y2 = ypf[nPoints-1]; + } else { + x1 = xp[nPoints-2]; + y1 = yp[nPoints-2]; + x2 = xp[nPoints-1]; + y2 = yp[nPoints-1]; + } + degrees = getFloatAngle(x1, y1, x2, y2); + if (tool!=Toolbar.ANGLE) { + Calibration cal = imp.getCalibration(); + double pw=cal.pixelWidth, ph=cal.pixelHeight; + if (IJ.altKeyDown()) {pw=1.0; ph=1.0;} + len = Math.sqrt((x2-x1)*pw*(x2-x1)*pw + (y2-y1)*ph*(y2-y1)*ph); + } + } + if (tool==Toolbar.ANGLE) { + if (nPoints==2) + angle1 = degrees; + else if (nPoints==3) { + double angle2 = xpf != null ? getFloatAngle(xpf[1], ypf[1], xpf[2], ypf[2]) : + getAngle(xp[1], yp[1], xp[2], yp[2]); + degrees = Math.abs(180-Math.abs(angle1-angle2)); + if (degrees>180.0) + degrees = 360.0-degrees; + } + } + String length = len!=-1?", length=" + IJ.d2s(len):""; + double degrees2 = tool==Toolbar.ANGLE&&nPoints==3&&Prefs.reflexAngle?360.0-degrees:degrees; + String angle = !Double.isNaN(degrees)?", angle=" + IJ.d2s(degrees2):""; + int ox = ic.offScreenX(sx); + int oy = ic.offScreenY(sy); + IJ.showStatus(imp.getLocationAsString(ox,oy) + length + angle); + } + + //Mouse behaves like an eraser when moved backwards with alt key down. + //Within correction circle, all vertices with sharp angles are removed. + //Norbert Vischer + protected void wipeBack() { + Roi prevRoi = Roi.getPreviousRoi(); + if (prevRoi!=null && prevRoi.modState==SUBTRACT_FROM_ROI) + return; + double correctionRadius = 20; + if (ic!=null) + correctionRadius /= ic.getMagnification(); + boolean found = false; + int p3 = nPoints - 1; + int p1 = p3; + while (p1 > 0 && !found) { + p1--; + double dx = xpf != null ? xpf[p3] - xpf[p1] : xp[p3] - xp[p1]; + double dy = xpf != null ? ypf[p3] - ypf[p1] : yp[p3] - yp[p1]; + double dist = Math.sqrt(dx * dx + dy * dy); + if (dist > correctionRadius) + found = true; + } + //examine all angles p1-p2-p3 + boolean killed = false; + int safety = 10; //don't delete more than this number of points at once + do { + killed = false; + safety--; + for (int p2 = p1 + 1; p2 < p3; p2++) { + double dx1 = xpf != null ? xpf[p2] - xpf[p1] : xp[p2] - xp[p1]; + double dy1 = xpf != null ? ypf[p2] - ypf[p1] : yp[p2] - yp[p1]; + double dx2 = xpf != null ? xpf[p3] - xpf[p1] : xp[p3] - xp[p1]; + double dy2 = xpf != null ? ypf[p3] - ypf[p1] : yp[p3] - yp[p1]; + double kk = 1;//allowed sharpness + if (this instanceof FreehandRoi) + kk = 0.8; + if ((dx1 * dx1 + dy1 * dy1) > kk * (dx2 * dx2 + dy2 * dy2)) { + if (xpf != null) { + xpf[p2] = xpf[p3]; ypf[p2] = ypf[p3]; //replace sharp vertex with end point + } else { + xp[p2] = xp[p3]; yp[p2] = yp[p3]; + } + p3 = p2; + nPoints = p2 + 1; //shorten array + killed = true; + } + } + } while (killed && safety > 0); + } + + void drawRubberBand(int sx, int sy) { + double oxd = offScreenXD(sx); + double oyd = offScreenYD(sy); + int ox = offScreenX(sx); + int oy = offScreenY(sy); + int x1, y1, x2, y2; + if (xpf!=null) { + x1 = (int)xpf[nPoints-2]+x; + y1 = (int)ypf[nPoints-2]+y; + x2 = (int)xpf[nPoints-1]+x; + y2 = (int)ypf[nPoints-1]+y; + } else { + x1 = xp[nPoints-2]+x; + y1 = yp[nPoints-2]+y; + x2 = xp[nPoints-1]+x; + y2 = yp[nPoints-1]+y; + } + int xmin=Integer.MAX_VALUE, ymin=Integer.MAX_VALUE, xmax=0, ymax=0; + if (x1xmax) xmax=x1; + if (x2>xmax) xmax=x2; + if (ox>xmax) xmax=ox; + if (y1ymax) ymax=y1; + if (y2>ymax) ymax=y2; + if (oy>ymax) ymax=oy; + int margin = boxSize; + if (ic!=null) { + double mag = ic.getMagnification(); + if (mag<1.0) margin = (int)(margin/mag); + } + margin = (int)(margin+getStrokeWidth()); + if (IJ.altKeyDown()) + margin+=20; + if (xpf!=null) { + xpf[nPoints-1] = (float)(oxd-getXBase()); + ypf[nPoints-1] = (float)(oyd-getYBase()); + } else { + xp[nPoints-1] = ox-x; + yp[nPoints-1] = oy-y; + } + if (type==POLYLINE && Prefs.splineFitLines) { + fitSpline(); + imp.draw(); + } else + imp.draw(xmin-margin, ymin-margin, (xmax-xmin)+margin*2, (ymax-ymin)+margin*2); + } + + void finishPolygon() { + if (xpf!=null) { + double xbase0 = getXBase(); + double ybase0 = getYBase(); + FloatPolygon poly = new FloatPolygon(xpf, ypf, nPoints); + bounds = poly.getFloatBounds(); + for (int i=0; i Math.abs(dy)) + dy = 0; + else + dx = 0; + sx = previousSX + dx; + sy = previousSY + dy; + } + + if (clipboard!=null) return; + + int ox = offScreenX(sx); + int oy = offScreenY(sy); + if (xpf!=null) { + xpf[activeHandle] = (float)(offScreenXD(sx)-getXBase()); + ypf[activeHandle] = (float)(offScreenYD(sy)-getYBase()); + } else { + xp[activeHandle] = ox-x; + yp[activeHandle] = oy-y; + } + if (xSpline!=null) { + fitSpline(splinePoints); + imp.draw(); + } else { + if (!subPixelResolution() || (type==POINT&&nPoints==1)) + resetBoundingRect(); + if (type==POINT && width==0 && height==0) + {width=1; height=1;} + updateClipRectAndDraw(); + } + String angle = type==ANGLE?getAngleAsString():""; + IJ.showStatus(imp.getLocationAsString(ox,oy) + angle); + } + + /** After handle is moved, find clip rect and repaint. */ + void updateClipRectAndDraw() { + if (xpf!=null) { + xp = toInt(xpf, xp, nPoints); + yp = toInt(ypf, yp, nPoints); + } + int xmin=Integer.MAX_VALUE, ymin=Integer.MAX_VALUE, xmax=0, ymax=0; + int x2, y2; + if (activeHandle>0) + {x2=x+xp[activeHandle-1]; y2=y+yp[activeHandle-1];} + else + {x2=x+xp[nPoints-1]; y2=y+yp[nPoints-1];} + if (x2xmax) xmax = x2; + if (y2>ymax) ymax = y2; + x2=x+xp[activeHandle]; y2=y+yp[activeHandle]; + if (x2xmax) xmax = x2; + if (y2>ymax) ymax = y2; + if (activeHandlexmax) xmax = x2; + if (y2>ymax) ymax = y2; + int xmin2=xmin, ymin2=ymin, xmax2=xmax, ymax2=ymax; + if (xClipMinxmax2) xmax2 = xClipMax; + if (yClipMax>ymax2) ymax2 = yClipMax; + xClipMin=xmin; yClipMin=ymin; xClipMax=xmax; yClipMax=ymax; + double mag = ic.getMagnification(); + int handleSize = type==POINT?getHandleSize()+25:getHandleSize(); + double strokeWidth = getStrokeWidth(); + if (strokeWidth<1.0) strokeWidth=1.0; + if (handleSizexmax) xmax=xx; + yy = yp[i]; + if (yyymax) ymax=yy; + } + if (xmin!=0) { + for (int i=0; i180.0) + degrees = 360.0-degrees; + double degrees2 = Prefs.reflexAngle&&type==ANGLE?360.0-degrees:degrees; + return ", angle=" + IJ.d2s(degrees2); + } + + protected void mouseDownInHandle(int handle, int sx, int sy) { + if (state==CONSTRUCTING) + return; + int ox = offScreenX(sx); + int oy = offScreenY(sy); + double oxd = offScreenXD(sx); + double oyd = offScreenYD(sy); + if ((IJ.altKeyDown()||IJ.controlKeyDown()) && !(nPoints<=3 && type!=POINT) && !(this instanceof RotatedRectRoi)) { + deleteHandle(oxd, oyd); + return; + } else if (IJ.shiftKeyDown() && type!=POINT && !(this instanceof RotatedRectRoi)) { + addHandle(oxd, oyd); + return; + } + super.mouseDownInHandle(handle, sx, sy); //sets state, activeHandle, previousSX&Y + int m = ic!=null?(int)(10.0/ic.getMagnification()):1; + xClipMin=ox-m; yClipMin=oy-m; xClipMax=ox+m; yClipMax=oy+m; + } + + public void deleteHandle(double ox, double oy) { + if (imp==null) + return; + if (nPoints<=1) { + imp.deleteRoi(); + return; + } + boolean splineFit = xSpline!=null; + if (splineFit) + removeSplineFit(); + FloatPolygon points = getFloatPolygon(); + int pointToDelete = getClosestPoint(ox, oy, points); + if (pointToDelete>=0) { + deletePoint(pointToDelete); + if (splineFit) + fitSpline(splinePoints); + imp.draw(); + } + } + + protected void deletePoint(int index) { + if (index<0 || index>=nPoints) + return; + for (int i=index; i=maxPoints) + enlargeArrays(); + float xbase = (float)getXBase(); + float ybase = (float)getYBase(); + if (xp==null) { + xp = new int[maxPoints]; + yp = new int[maxPoints]; + } + for (int i=0; i= 0.0 || i==npoints-1) && pointsWritten < npOut) { // we have to write a new point + double fractionOverNextWrite = distanceOverNextWrite/distance; + if (distance==0) fractionOverNextWrite = 0; + //IJ.log("i="+i+" n="+pointsWritten+"/"+npOut+" leng="+IJ.d2s(lengthRead)+"/"+IJ.d2s(length)+" done="+IJ.d2s(pointsWritten*step)+" over="+IJ.d2s(fractionOverNextWrite)+" x,y="+IJ.d2s(x2 - fractionOverNextWrite*dx)+","+IJ.d2s(y2 - fractionOverNextWrite*dy)); + xpOut[pointsWritten] = (float)(x2 - fractionOverNextWrite*dx); + ypOut[pointsWritten] = (float)(y2 - fractionOverNextWrite*dy); + distanceOverNextWrite -= step; + pointsWritten++; + } + } + return new float[][] {xpOut, ypOut, new float[] {(float)step}}; + } + + /** With segmented selections, ignore first mouse up and finalize + when user double-clicks, control-clicks or clicks in start box. */ + protected void handleMouseUp(int sx, int sy) { + if (state==MOVING) { + state = NORMAL; + return; + } + if (state==MOVING_HANDLE) { + cachedMask = null; //mask is no longer valid + state = NORMAL; + updateClipRect(); + oldX=x; oldY=y; + oldWidth=width; oldHeight=height; + if (subPixelResolution()) + resetBoundingRect(); + return; + } + if (state!=CONSTRUCTING) + return; + if (IJ.spaceBarDown()) // is user scrolling image? + return; + boolean samePoint = false; + if (xpf!=null) + samePoint = (xpf[nPoints-2]==xpf[nPoints-1] && ypf[nPoints-2]==ypf[nPoints-1]); + else + samePoint = (xp[nPoints-2]==xp[nPoints-1] && yp[nPoints-2]==yp[nPoints-1]); + boolean doubleClick = (System.currentTimeMillis()-mouseUpTime)<=300; + int size = boxSize+2; + int size2 = boxSize/2 +1; + Rectangle biggerStartBox = new Rectangle(screenXD(startXD)-5, screenYD(startYD)-5, 10, 10); + if (nPoints>2 && (biggerStartBox.contains(sx, sy) + || (offScreenXD(sx)==startXD && offScreenYD(sy)==startYD) + || (samePoint && doubleClick))) { + boolean okayToFinish = true; + if (type==POLYGON && samePoint && doubleClick && nPoints>25) { + okayToFinish = IJ.showMessageWithCancel("Polygon Tool", "Complete the selection?"); + } + if (okayToFinish) { + nPoints--; + addOffset(); + finishPolygon(); + return; + } + } else if (!samePoint) { + mouseUpTime = System.currentTimeMillis(); + if (type==ANGLE && nPoints==3) { + addOffset(); + finishPolygon(); + return; + } + //add point to polygon + if (xpf!=null) { + xpf[nPoints] = xpf[nPoints-1]; + ypf[nPoints] = ypf[nPoints-1]; + nPoints++; + if (nPoints==xpf.length) + enlargeArrays(); + } else { + xp[nPoints] = xp[nPoints-1]; + yp[nPoints] = yp[nPoints-1]; + nPoints++; + if (nPoints==xp.length) + enlargeArrays(); + } + if (constrain) { // this point was constrained in 90deg steps; correct coordinates + int dx = sx - previousSX; + int dy = sy - previousSY; + if (Math.abs(dx) > Math.abs(dy)) + dy = 0; + else + dx = 0; + sx = previousSX + dx; + sy = previousSY + dy; + } + previousSX = sx; //save for constraining next line if desired + previousSY = sy; + notifyListeners(RoiListener.EXTENDED); + } + } + + protected void addOffset() { + if (xpf!=null) { + float xbase = (float)getXBase(); + float ybase = (float)getYBase(); + for (int i=0; i=sx2 && sx<=sx2+size && sy>=sy2 && sy<=sy2+size) { + handle = i; + break; + } + } + return handle; + } + + public ImageProcessor getMask() { + ImageProcessor mask = cachedMask; + if (mask!=null && mask.getPixels()!=null + && mask.getWidth()==width && mask.getHeight()==height) + return mask; + PolygonFiller pf = new PolygonFiller(); + if (xSpline!=null) + pf.setPolygon(xSpline, ySpline, splinePoints, getXBase()-x, getYBase()-y); + else if (xpf!=null) + pf.setPolygon(xpf, ypf, nPoints, getXBase()-x, getYBase()-y); + else + pf.setPolygon(xp, yp, nPoints); + mask = pf.getMask(width, height); + cachedMask = mask; + return mask; + } + + /** Returns the length of this line selection after + smoothing using a 3-point running average.*/ + double getSmoothedLineLength(ImagePlus imp) { + if (subPixelResolution() && xpf!=null) + return getFloatSmoothedLineLength(imp); + double length = 0.0; + double w2 = 1.0; + double h2 = 1.0; + double dx, dy; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + w2 = cal.pixelWidth*cal.pixelWidth; + h2 = cal.pixelHeight*cal.pixelHeight; + } + dx = (xp[0]+xp[1]+xp[2])/3.0-xp[0]; + dy = (yp[0]+yp[1]+yp[2])/3.0-yp[0]; + length += Math.sqrt(dx*dx*w2+dy*dy*h2); + for (int i=1; i1 || !corner) { + corner = true; + nCorners++; + } else + corner = false; + dx1 = dx2; + dy1 = dy2; + side1 = side2; + } + double w=1.0,h=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + w = cal.pixelWidth; + h = cal.pixelHeight; + } + return sumdx*w+sumdy*h-(nCorners*((w+h)-Math.sqrt(w*w+h*h))); + /* Alternative code leading to slightly different results: + * It does this by calculating the total length of the ROI boundary + * and subtracting 1-sqrt(2)/2 for each corner. + * For example, a 1x1 pixel ROI has a boundary length of 4 and + * and 4 corners so the perimeter is 4-4*(1-sqrt(2)/2) = 2*sqrt(2). + * A 2x2 pixel ROI has a boundary length of 8 and 4 corners so the + * perimeter is 8-4*(1-sqrt(2)/2) = 4 + 2*sqrt(2). /* int x0 = xp[nPoints-2]; + int y0 = yp[nPoints-2]; + int x1 = xp[nPoints-1]; + int y1 = yp[nPoints-1]; + int sumdx = 0; + int sumdy = 0; + int nCorners = 0; + for (int i=0; i2) { + if (type==FREEROI) + return getSmoothedPerimeter(imp); + else if (type==FREELINE && !(width==0 || height==0)) + return getSmoothedLineLength(imp); + } + + boolean closeShape = isArea(); + if (xSpline!=null) + return getLength(xSpline, ySpline, splinePoints, closeShape, imp); + else if (xpf!=null) + return getLength(xpf, ypf, nPoints, closeShape, imp); + else + return getLength(xp, yp, nPoints, closeShape, imp); + } + + /** Returns the length of a polygon with integer coordinates. Uses no calibration if imp is null. */ + static double getLength(int[] xpoints, int[] ypoints, int npoints, boolean closeShape, ImagePlus imp) { + if (npoints < 2) return 0; + double pixelWidth = 1.0, pixelHeight = 1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pixelWidth = cal.pixelWidth; + pixelHeight = cal.pixelHeight; + } + double length = 0; + for (int i=0; i "+newSize); + maxPoints = newSize; + } + + public void setLocation(double x, double y) { + super.setLocation(x, y); + if ((int)x!=x || (int)y!=y) + enableSubPixelResolution(); + } + + public void enableSubPixelResolution() { + super.enableSubPixelResolution(); + if (xpf==null && xp!=null) { + xpf = toFloat(xp); + ypf = toFloat(yp); + } + } + + public String getDebugInfo() { + String s = "ROI Debug Properties\n"; + s += " bounds: "+bounds+"\n"; + s += " x,y,w,h: "+x+","+y+","+width+","+height+"\n"; + if (xpf!=null && xpf.length>0) + s += " xpf[0],ypf[0]: "+xpf[0]+","+ypf[0]+"\n"; + return s; + } + +} diff --git a/src/ij/gui/ProfilePlot.java b/src/ij/gui/ProfilePlot.java new file mode 100644 index 0000000..875a295 --- /dev/null +++ b/src/ij/gui/ProfilePlot.java @@ -0,0 +1,341 @@ +package ij.gui; + +import java.awt.*; +import java.util.ArrayList; +import ij.*; +import ij.process.*; +import ij.util.*; +import ij.measure.*; +import ij.plugin.Straightener; + +/** Creates a density profile plot of a rectangular selection or line selection. */ +public class ProfilePlot { + + static final int MIN_WIDTH = 350; + static final double ASPECT_RATIO = 0.5; + private double min, max; + private boolean minAndMaxCalculated; + private static double fixedMin; + private static double fixedMax; + + protected ImagePlus imp; + protected double[] profile; + protected double magnification; + protected double xInc; + protected String units; + protected String yLabel; + protected float[] xValues; + + + public ProfilePlot() { + } + + public ProfilePlot(ImagePlus imp) { + this(imp, false); + } + + public ProfilePlot(ImagePlus imp, boolean averageHorizontally) { + this.imp = imp; + Roi roi = imp.getRoi(); + if (roi==null) { + IJ.error("Profile Plot", "Selection required."); + return; + } + int roiType = roi.getType(); + if (!(roi.isLine() || roiType==Roi.RECTANGLE)) { + IJ.error("Line or rectangular selection required."); + return; + } + Calibration cal = imp.getCalibration(); + xInc = cal.pixelWidth; + units = cal.getUnits(); + yLabel = cal.getValueUnit(); + ImageProcessor ip = imp.getProcessor(); + if (roiType==Roi.LINE) + profile = getStraightLineProfile(roi, cal, ip); + else if (roiType==Roi.POLYLINE || roiType==Roi.FREELINE) { + int lineWidth = (int)Math.round(roi.getStrokeWidth()); + if (lineWidth<=1) + profile = getIrregularProfile(roi, ip, cal); + else + profile = getWideLineProfile(imp, lineWidth); + } else if (averageHorizontally) + profile = getRowAverageProfile(roi.getBounds(), cal, ip); + else + profile = getColumnAverageProfile(roi.getBounds(), ip); + ip.setCalibrationTable(null); + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) + magnification = ic.getMagnification(); + else + magnification = 1.0; + } + + /** Returns the size of the plot that createWindow() creates. */ + public Dimension getPlotSize() { + if (profile==null) return null; + int width = (int)(profile.length*magnification); + int height = (int)(width*ASPECT_RATIO); + if (widthmaxWidth) { + width = maxWidth; + height = (int)(width*ASPECT_RATIO); + } + return new Dimension(width, height); + } + + /** Displays this profile plot in a window. */ + public void createWindow() { + Plot plot = getPlot(); + if (plot!=null) + plot.show(); + } + + public Plot getPlot() { + if (profile==null) + return null; + String xLabel = "Distance ("+units+")"; + int n = profile.length; + if (xValues==null) { + xValues = new float[n]; + for (int i=0; i0 && (title.length()-index)<=5) + title = title.substring(0, index); + return title; + } + + /** Returns the profile plot data. */ + public double[] getProfile() { + return profile; + } + + /** Returns the calculated minimum value. */ + public double getMin() { + if (!minAndMaxCalculated) + findMinAndMax(); + return min; + } + + /** Returns the calculated maximum value. */ + public double getMax() { + if (!minAndMaxCalculated) + findMinAndMax(); + return max; + } + + /** Sets the y-axis min and max. Specify (0,0) to autoscale. */ + public static void setMinAndMax(double min, double max) { + fixedMin = min; + fixedMax = max; + IJ.register(ProfilePlot.class); + } + + /** Returns the profile plot y-axis min. Auto-scaling is used if min=max=0. */ + public static double getFixedMin() { + return fixedMin; + } + + /** Returns the profile plot y-axis max. Auto-scaling is used if min=max=0. */ + public static double getFixedMax() { + return fixedMax; + } + + double[] getStraightLineProfile(Roi roi, Calibration cal, ImageProcessor ip) { + ip.setInterpolate(PlotWindow.interpolate); + Line line = (Line)roi; + double[] values = line.getPixels(); + if (values==null) return null; + if (cal!=null && cal.pixelWidth!=cal.pixelHeight) { + FloatPolygon p = line.getFloatPoints(); + double dx = p.xpoints[1] - p.xpoints[0]; + double dy = p.ypoints[1] - p.ypoints[0]; + double pixelLength = Math.sqrt(dx*dx + dy*dy); + dx = cal.pixelWidth*dx; + dy = cal.pixelHeight*dy; + double calibratedLength = Math.sqrt(dx*dx + dy*dy); + xInc = calibratedLength * 1.0/pixelLength; + } + return values; + } + + double[] getRowAverageProfile(Rectangle rect, Calibration cal, ImageProcessor ip) { + double[] profile = new double[rect.height]; + int[] counts = new int[rect.height]; + double[] aLine; + ip.setInterpolate(false); + for (int x=rect.x; xsubpixel resolution), + * the line coordinates are interpreted as the roi line shown at high zoom level, + * i.e., integer (x,y) is at the top left corner of pixel (x,y). + * Thus, the coordinates of the pixel center are taken as (x+0.5, y+0.5). + * If subpixel resolution if off, the coordinates of the pixel centers are taken + * as integer (x,y). */ + double[] getIrregularProfile(Roi roi, ImageProcessor ip, Calibration cal) { + boolean interpolate = PlotWindow.interpolate; + boolean calcXValues = cal!=null && cal.pixelWidth!=cal.pixelHeight; + FloatPolygon p = roi.getFloatPolygon(); + int n = p.npoints; + float[] xpoints = p.xpoints; + float[] ypoints = p.ypoints; + ArrayList values = new ArrayList(); + int n2; + double inc = 0.01; + double distance=0.0, distance2=0.0, dx=0.0, dy=0.0, xinc, yinc; + double x, y, lastx=0.0, lasty=0.0, x1, y1, x2=xpoints[0], y2=ypoints[0]; + double value; + for (int i=1; i=1.0-inc/2.0) { + if (interpolate) + value = ip.getInterpolatedValue(x, y); + else + value = ip.getPixelValue((int)Math.round(x), (int)Math.round(y)); + values.add(new Double(value)); + lastx=x; lasty=y; + } + x += xinc; + y += yinc; + } while (--n2>0); + } + double[] values2 = new double[values.size()]; + for (int i=0; imax) + max = value; + } + this.min = min; + this.max = max; + } + + +} diff --git a/src/ij/gui/ProgressBar.java b/src/ij/gui/ProgressBar.java new file mode 100644 index 0000000..7b5ee91 --- /dev/null +++ b/src/ij/gui/ProgressBar.java @@ -0,0 +1,165 @@ +package ij.gui; + +import ij.macro.Interpreter; +import java.awt.*; +import java.awt.image.*; + +/** + * This is the progress bar that is displayed in the lower right hand corner of + * the ImageJ window. Use one of the static IJ.showProgress() methods to display + * and update the progress bar. + */ +public class ProgressBar extends Canvas { + + public static final int WIDTH = 120; + public static final int HEIGHT = 20; + + private int canvasWidth, canvasHeight; + private int x, y, width, height; + private long lastTime = 0; + private boolean showBar; + private boolean batchMode; + + private Color barColor = Color.gray; + private Color fillColor = new Color(204, 204, 255); + private Color backgroundColor = ij.ImageJ.backgroundColor; + private Color frameBrighter = backgroundColor.brighter(); + private Color frameDarker = backgroundColor.darker(); + private boolean dualDisplay = false; + private double slowX = 0.0;//box + private double fastX = 0.0;//dot + + /** + * This constructor is called once by ImageJ at startup. + */ + public ProgressBar(int canvasWidth, int canvasHeight) { + init(canvasWidth, canvasHeight); + } + + public void init(int canvasWidth, int canvasHeight) { + this.canvasWidth = canvasWidth; + this.canvasHeight = canvasHeight; + x = 3; + y = 5; + width = canvasWidth - 8; + height = canvasHeight - 7; + } + + void fill3DRect(Graphics g, int x, int y, int width, int height) { + g.setColor(fillColor); + g.fillRect(x + 1, y + 1, width - 2, height - 2); + g.setColor(frameDarker); + g.drawLine(x, y, x, y + height); + g.drawLine(x + 1, y, x + width - 1, y); + g.setColor(frameBrighter); + g.drawLine(x + 1, y + height, x + width, y + height); + g.drawLine(x + width, y, x + width, y + height - 1); + } + + /** + * Updates the progress bar, where abs(progress) should run from 0 to 1. + * If abs(progress) == 1 the bar is erased. The bar is updated only + * if more than 90 ms have passed since the last call. Does nothing if the + * ImageJ window is not present. + * @param progress Length of the progress bar to display (0...1). + * Using progress with negative sign (0 .. -1) will regard subsequent calls with + * positive argument as sub-ordinate processes that are displayed as moving dot. + */ + public void show(double progress) { + show(progress, false); + } + + /** + * Updates the progress bar, where abs(progress) should run from 0 to 1. + * @param progress Length of the progress bar to display (0...1). + * @param showInBatchMode show progress bar in batch mode macros? + */ + public void show(double progress, boolean showInBatchMode) { + boolean finished = false; + if (progress<=-1) + finished = true; + if (!dualDisplay && progress >= 1) + finished = true; + if (!finished) { + if (progress < 0) { + slowX = -progress; + fastX = 0.0; + dualDisplay = true; + } else if (dualDisplay) + fastX = progress; + if (!dualDisplay) + slowX = progress; + } + if (!showInBatchMode && (batchMode || Interpreter.isBatchMode())) + return; + if (finished) {//clear the progress bar + slowX = 0.0; + fastX = 0.0; + showBar = false; + dualDisplay = false; + repaint(); + return; + } + long time = System.currentTimeMillis(); + if (time-lastTime<90 && progress!=1.0) + return; + lastTime = time; + showBar = true; + repaint(); + } + + /** + * Updates the progress bar, where the length of the bar is set to + * ((abs(currentIndex)+1)/abs(finalIndex) of the maximum bar + * length. Use a negative currentIndex to show subsequent + * plugin calls as moving dot. The bar is erased if + * currentIndex>=finalIndex-1 or finalIndex == 0. + */ + public void show(int currentIndex, int finalIndex) { + boolean wasNegative = currentIndex < 0; + double progress = ((double) Math.abs(currentIndex) + 1.0) / Math.abs(finalIndex); + if (wasNegative) + progress = -progress; + if (finalIndex == 0) + progress = -1; + show(progress); + } + + public void update(Graphics g) { + paint(g); + } + + public void paint(Graphics g) { + if (showBar) { + fill3DRect(g, x - 1, y - 1, width + 1, height + 1); + drawBar(g); + } else { + g.setColor(backgroundColor); + g.fillRect(0, 0, canvasWidth, canvasHeight); + } + } + + void drawBar(Graphics g) { + int barEnd = (int) (width * slowX); + if (Toolbar.getToolId()==Toolbar.ANGLE) + g.setColor(Color.getHSBColor(((float)(System.currentTimeMillis()%1000))/1000, 0.5f, 1.0f)); + else + g.setColor(barColor); + g.fillRect(x, y, barEnd, height); + if (dualDisplay && fastX > 0) { + int dotPos = (int) (width * fastX); + g.setColor(Color.BLACK); + if (dotPos > 1 && dotPos < width - 7) + g.fillOval(dotPos, y + 3, 7, 7); + } + } + + public Dimension getPreferredSize() { + return new Dimension(canvasWidth, canvasHeight); + } + + public void setBatchMode(boolean batchMode) { + this.batchMode = batchMode; + } + +} diff --git a/src/ij/gui/Roi.java b/src/ij/gui/Roi.java new file mode 100644 index 0000000..a986810 --- /dev/null +++ b/src/ij/gui/Roi.java @@ -0,0 +1,2957 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.*; +import ij.plugin.frame.Recorder; +import ij.plugin.filter.Analyzer; +import ij.plugin.filter.ThresholdToSelection; +import ij.macro.Interpreter; +import ij.io.RoiDecoder; +import java.awt.*; +import java.util.*; +import java.io.*; +import java.awt.image.*; +import java.awt.event.*; +import java.awt.geom.*; + +/** + * A rectangular region of interest and superclass for the other ROI classes. + * + * This class implements {@code Iterable} and can thus be + * used to iterate over the contained coordinates. Usage example: + *
+ * Roi roi = ...;
+ * for (Point p : roi) {
+ *   // process p
+ * }
+ * 
+ * + * Convention for subpixel resolution and zooming in: + *
    + *
  • Area ROIs: Integer coordinates refer to the top-left corner of the pixel with these coordinates. + * Thus, pixel (0,0) is enclosed by the rectangle spanned between points (0,0) and (1,1), + * i.e., a rectangle at (0,0) with width = height = 1 pixel. + *
  • Line and Point Rois: Integer coordinates refer to the center of a pixel. + * Thus, a line from (0,0) to (1,0) has its start and end points in the center of + * pixels (0,0) and (1,0), respectively, and drawing the line should affect both + * pixels. For images dispplayed at high zoom levels, this means that (open) lines + * and single points are displayed 0.5 pixels further to the right and bottom than + * the outlines of area ROIs (closed lines) with the same coordinates. + *
+ * Note that rectangular and (nonrotated) oval ROIs do not support subpixel resolution. + * Since ImageJ 1.52t, this convention does not depend on the Prefs.subpixelResolution + * (previously accessible via Edit>Options>Plot) and this flag has no effect any more. + * + */ +public class Roi extends Object implements Cloneable, java.io.Serializable, Iterable { + + public static final int CONSTRUCTING=0, MOVING=1, RESIZING=2, NORMAL=3, MOVING_HANDLE=4; // States + public static final int RECTANGLE=0, OVAL=1, POLYGON=2, FREEROI=3, TRACED_ROI=4, LINE=5, + POLYLINE=6, FREELINE=7, ANGLE=8, COMPOSITE=9, POINT=10; // Types + public static final int HANDLE_SIZE = 5; // replaced by getHandleSize() + public static final int NOT_PASTING = -1; + public static final int FERET_ARRAYSIZE = 16; // Size of array with Feret values + public static final int FERET_ARRAY_POINTOFFSET = 8; // Where point coordinates start in Feret array + private static final String NAMES_KEY = "group.names"; + + static final int NO_MODS=0, ADD_TO_ROI=1, SUBTRACT_FROM_ROI=2; // modification states + + int startX, startY, x, y, width, height; + double startXD, startYD; + Rectangle2D.Double bounds; + int activeHandle; + int state; + int modState = NO_MODS; + int cornerDiameter; //for rounded rectangle + int previousSX, previousSY; //remember for aborting moving with esc and constrain + + public static final BasicStroke onePixelWide = new BasicStroke(1); + protected static Color ROIColor = Prefs.getColor(Prefs.ROICOLOR,Color.yellow); + protected static int pasteMode = Blitter.COPY; + protected static int lineWidth = 1; + protected static Color defaultFillColor; + private static Vector listeners = new Vector(); + private static LUT glasbeyLut; + private static int defaultGroup; // zero is no specific group + private static Color groupColor; + private static double defaultStrokeWidth; + private static String groupNamesString = Prefs.get(NAMES_KEY, null); + private static String[] groupNames; + private static boolean groupNamesChanged; + + /** Get using getPreviousRoi() and set using setPreviousRoi() */ + public static Roi previousRoi; + + protected int type; + protected int xMax, yMax; + protected ImagePlus imp; + private int imageID; + protected ImageCanvas ic; + protected int oldX, oldY, oldWidth, oldHeight; //remembers previous clip rect + protected int clipX, clipY, clipWidth, clipHeight; + protected ImagePlus clipboard; + protected boolean constrain; // to be square or limit to horizontal/vertical motion + protected boolean center; + protected boolean aspect; + protected boolean updateFullWindow; + protected double mag = 1.0; + protected double asp_bk; //saves aspect ratio if resizing takes roi very small + protected ImageProcessor cachedMask; + protected Color handleColor = Color.white; + protected Color strokeColor; + protected Color instanceColor; //obsolete; replaced by strokeColor + protected Color fillColor; + protected BasicStroke stroke; + protected boolean nonScalable; + protected boolean overlay; + protected boolean wideLine; + protected boolean ignoreClipRect; + protected double flattenScale = 1.0; + protected static Color defaultColor; + + private String name; + private int position; + private int channel, slice, frame; + private boolean hyperstackPosition; + private Overlay prototypeOverlay; + private boolean subPixel; + private boolean activeOverlayRoi; + private Properties props; + private boolean isCursor; + private double xcenter = Double.NaN; + private double ycenter; + private boolean listenersNotified; + private boolean antiAlias = true; + private int group; + private boolean usingDefaultStroke; + private static int defaultHandleSize; + private int handleSize = -1; + private boolean scaleStrokeWidth; // Scale stroke width when zooming images? + + /** Creates a rectangular ROI. */ + public Roi(int x, int y, int width, int height) { + this(x, y, width, height, 0); + } + + /** Creates a rectangular ROI using double arguments. */ + public Roi(double x, double y, double width, double height) { + this(x, y, width, height, 0); + } + + /** Creates a new rounded rectangular ROI. */ + public Roi(int x, int y, int width, int height, int cornerDiameter) { + setImage(null); + if (width<1) width = 1; + if (height<1) height = 1; + if (width>xMax) width = xMax; + if (height>yMax) height = yMax; + this.cornerDiameter = cornerDiameter; + this.x = x; + this.y = y; + startX = x; startY = y; + oldX = x; oldY = y; oldWidth=0; oldHeight=0; + this.width = width; + this.height = height; + oldWidth=width; + oldHeight=height; + clipX = x; + clipY = y; + clipWidth = width; + clipHeight = height; + state = NORMAL; + type = RECTANGLE; + if (ic!=null) { + Graphics g = ic.getGraphics(); + draw(g); + g.dispose(); + } + double defaultWidth = defaultStrokeWidth(); + if (defaultWidth>0) { + stroke = new BasicStroke((float)defaultWidth); + usingDefaultStroke = true; + } + fillColor = defaultFillColor; + this.group = defaultGroup; //initialize with current group and associated color + if (defaultGroup>0) + this.strokeColor = groupColor; + } + + /** Creates a rounded rectangular ROI using double arguments. */ + public Roi(double x, double y, double width, double height, int cornerDiameter) { + this((int)x, (int)y, (int)Math.ceil(width), (int)Math.ceil(height), cornerDiameter); + bounds = new Rectangle2D.Double(x, y, width, height); + subPixel = true; + } + + /** Creates a new rectangular Roi. */ + public Roi(Rectangle r) { + this(r.x, r.y, r.width, r.height); + } + + /** Starts the process of creating a user-defined rectangular Roi, + where sx and sy are the starting screen coordinates. */ + public Roi(int sx, int sy, ImagePlus imp) { + this(sx, sy, imp, 0); + } + + /** Starts the process of creating a user-defined rectangular Roi, + where sx and sy are the starting screen coordinates. + For rectangular rois, also a corner diameter may be specified to + make it a rounded rectangle */ + public Roi(int sx, int sy, ImagePlus imp, int cornerDiameter) { + setImage(imp); + int ox=sx, oy=sy; + if (ic!=null) { + ox = ic.offScreenX2(sx); + oy = ic.offScreenY2(sy); + } + setLocation(ox, oy); + this.cornerDiameter = cornerDiameter; + width = 0; + height = 0; + state = CONSTRUCTING; + type = RECTANGLE; + if (cornerDiameter>0) { + double swidth = RectToolOptions.getDefaultStrokeWidth(); + if (swidth>0.0) + setStrokeWidth(swidth); + Color scolor = RectToolOptions.getDefaultStrokeColor(); + if (scolor!=null) + setStrokeColor(scolor); + } + double defaultWidth = defaultStrokeWidth(); + if (defaultWidth>0) { + stroke = new BasicStroke((float)defaultWidth); + usingDefaultStroke = true; + } + fillColor = defaultFillColor; + this.group = defaultGroup; + if (defaultGroup>0) + this.strokeColor = groupColor; + } + + /** Creates a rectangular ROI. */ + public static Roi create(double x, double y, double width, double height) { + return new Roi(x, y, width, height); + } + + /** Creates a rounded rectangular ROI. */ + public static Roi create(double x, double y, double width, double height, int cornerDiameter) { + return new Roi(x, y, width, height, cornerDiameter); + } + + /** @deprecated */ + public Roi(int x, int y, int width, int height, ImagePlus imp) { + this(x, y, width, height); + setImage(imp); + } + + /** Set the location of the ROI in image coordinates. */ + public void setLocation(int x, int y) { + this.x = x; + this.y = y; + startX = x; startY = y; + oldX = x; oldY = y; oldWidth=0; oldHeight=0; + if (bounds!=null) { + if (!isInteger(bounds.x) || !isInteger(bounds.y)) { + cachedMask = null; + width = (int)Math.ceil(bounds.width); + height = (int)Math.ceil(bounds.height); + } + bounds.x = x; + bounds.y = y; + if (this instanceof PolygonRoi) setIntBounds(bounds); + } + } + + /** Set the location of the ROI in image coordinates. */ + public void setLocation(double x, double y) { + setLocation((int)x, (int)y); + if (isInteger(x) && isInteger(y)) + return; + if (bounds!=null) { + if (!isInteger(x-bounds.x) || !isInteger(y-bounds.y)) { + cachedMask = null; + width = (int)Math.ceil(bounds.x + bounds.width) - this.x; //ensure that all pixels are inside + height = (int)Math.ceil(bounds.y + bounds.height) - this.y; + } + bounds.x = x; + bounds.y = y; + } else { + cachedMask = null; + bounds = new Rectangle2D.Double(x, y, width, height); + } + if (this instanceof PolygonRoi) setIntBounds(bounds); + subPixel = true; + } + + /** Sets the ImagePlus associated with this ROI. + * imp may be null to remove the association to an image. */ + public void setImage(ImagePlus imp) { + this.imp = imp; + cachedMask = null; + if (imp==null) { + ic = null; + clipboard = null; + xMax = yMax = Integer.MAX_VALUE; + } else { + ic = imp.getCanvas(); + xMax = imp.getWidth(); + yMax = imp.getHeight(); + } + } + + /** Returns the ImagePlus associated with this ROI, or null. */ + public ImagePlus getImage() { + return imp; + } + + /** Returns the ID of the image associated with this ROI. */ + public int getImageID() { + return imp!=null?imp.getID():imageID; + } + + public int getType() { + return type; + } + + public int getState() { + return state; + } + + /** Returns the perimeter length. */ + public double getLength() { + double pw=1.0, ph=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } + double perimeter = 2.0*width*pw + 2.0*height*ph; + if (cornerDiameter > 0) { //using Ramanujan's approximation for the circumference of an ellipse + double a = 0.5*Math.min(cornerDiameter, width)*pw; + double b = 0.5*Math.min(cornerDiameter, height)*ph; + perimeter += Math.PI*(3*(a + b) - Math.sqrt((3*a + b)*(a + 3*b))) -4*(a+b); + } + return perimeter; + } + + /** Returns Feret's diameter, the greatest distance between + any two points along the ROI boundary. */ + public double getFeretsDiameter() { + double[] a = getFeretValues(); + return a!=null?a[0]:0.0; + } + + /** Returns an array with the following values: + *
[0] "Feret" (maximum caliper width) + *
[1] "FeretAngle" (angle of diameter with maximum caliper width, between 0 and 180 deg) + *
[2] "MinFeret" (minimum caliper width) + *
[3][4] , "FeretX" and "FeretY", the X and Y coordinates of the starting point + * (leftmost point) of the maximum-caliper-width diameter. + *
[5-7] reserved + *
All these values and point coordinates are in calibrated image coordinates. + *

+ * The following array elements are end points of the maximum and minimum caliper diameter, + * in unscaled image pixel coordinates: + *
[8][9] "FeretX1", "FeretY1"; unscaled versions of "FeretX" and "FeretY" + * (subclasses may use any end of the diameter, not necessarily the left one) + *
[10][11] "FeretX2", "FeretY2", end point of the maxium-caliper-width diameter. + * Both of these points are vertices of the convex hull. + *
The final four array elements are the starting and end points of the minimum caliper width, + *
[12],[13] "MinFeretX", "MinFeretY", and + *
[14],[15] "MinFeretX2", "MinFeretY2". These two pooints are not sorted by x, + * but the first point point (MinFeretX, MinFeretY) is guaranteed to be a vertex of the convex hull, + * while second point (MinFeretX2, MinFeretY2) usually is not a vertex point but at a + * boundary line of the convex hull. */ + public double[] getFeretValues() { + double pw=1.0, ph=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } + + FloatPolygon poly = getFloatConvexHull(); + if (poly==null || poly.npoints==0) return null; + + double[] a = new double[FERET_ARRAYSIZE]; + // calculate maximum Feret diameter: largest distance between any two points + int p1=0, p2=0; + double diameterSqr = 0.0; //square of maximum Feret diameter + for (int i=0; idiameterSqr) {diameterSqr=dsqr; p1=i; p2=j;} + } + } + if (poly.xpoints[p1] > poly.xpoints[p2]) { + int p2swap = p1; p1 = p2; p2 = p2swap; + } + double xf1=poly.xpoints[p1], yf1=poly.ypoints[p1]; + double xf2=poly.xpoints[p2], yf2=poly.ypoints[p2]; + double angle = (180.0/Math.PI)*Math.atan2((yf1-yf2)*ph, (xf2-xf1)*pw); + if (angle < 0.0) + angle += 180.0; + a[0] = Math.sqrt(diameterSqr); + a[1] = angle; + a[3] = xf1; a[4] = yf1; + { int i = FERET_ARRAY_POINTOFFSET; //array elements 8-11 are start and end points of max Feret diameter + a[i++] = poly.xpoints[p1]; a[i++] = poly.ypoints[p1]; + a[i++] = poly.xpoints[p2]; a[i++] = poly.ypoints[p2]; + } + + // Calculate minimum Feret diameter: + // For all pairs of points on the convex hull: + // Get the point with the largest distance from the line between these two points + // Of all these pairs, take the one where the distance is the lowest + // The following code requires a counterclockwise convex hull with no duplicate points + double x0 = poly.xpoints[poly.npoints-1]; + double y0 = poly.ypoints[poly.npoints-1]; + double minFeret = Double.MAX_VALUE; + double[] xyEnd = new double[4]; //start and end points of the minFeret diameter, uncalibrated + double[] xyEi = new double[4]; //intermediate values of xyEnd + for (int i=0; i maxDist) { + maxDist = dist; + xyEi[0] = x1; + xyEi[1] = y1; + xyEi[2] = xyEi[0] - (xnorm/pw * dist)/pw; + xyEi[3] = xyEi[1] - (ynorm/ph * dist)/ph; + } + } + if (maxDist < minFeret) { + minFeret = maxDist; + System.arraycopy(xyEi, 0, xyEnd, 0, 4); + } + } + a[2] = minFeret; + System.arraycopy(xyEnd, 0, a, FERET_ARRAY_POINTOFFSET+4, 4); //a[12]-a[15] are minFeretX, Y, X2, Y2 + return a; + } + + /** Returns the convex hull of this Roi as a Polygon with integer coordinates + * by rounding the floating-point values. + * Coordinates of the convex hull are image pixel coordinates. */ + public Polygon getConvexHull() { + FloatPolygon fp = getFloatConvexHull(); + return new Polygon(toIntR(fp.xpoints), toIntR(fp.ypoints), fp.npoints); + } + + /** Returns the convex hull of this Roi as a FloatPolygon. + * Coordinates of the convex hull are image pixel coordinates. */ + public FloatPolygon getFloatConvexHull() { + FloatPolygon fp = getFloatPolygon(""); //no duplicate closing points, no path-separating NaNs needed + return fp == null ? null : fp.getConvexHull(); + } + + double getFeretBreadth(Shape shape, double angle, double x1, double y1, double x2, double y2) { + double cx = x1 + (x2-x1)/2; + double cy = y1 + (y2-y1)/2; + AffineTransform at = new AffineTransform(); + at.rotate(angle*Math.PI/180.0, cx, cy); + Shape s = at.createTransformedShape(shape); + Rectangle2D r = s.getBounds2D(); + return Math.min(r.getWidth(), r.getHeight()); + } + + /** Returns this selection's bounding rectangle. */ + public Rectangle getBounds() { + return new Rectangle(x, y, width, height); + } + + /** Returns this selection's bounding rectangle (with subpixel accuracy). */ + public Rectangle2D.Double getFloatBounds() { + if (bounds!=null) + return new Rectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height); + else + return new Rectangle2D.Double(x, y, width, height); + } + + /** Sets the bounds of rectangular, oval or text selections. + * Note that for these types, subpixel resolution is ignored, + * and the x,y values are rounded down, the width and height values rounded up. + * Do not use for other ROI types since their width and height are results of + * a calculation. + * For translating ROIs, use setLocation. */ + public void setBounds(Rectangle2D.Double b) { + if (!(type==RECTANGLE||type==OVAL||(this instanceof TextRoi))) + return; + this.x = (int)b.x; + this.y = (int)b.y; + this.width = (int)Math.ceil(b.width); + this.height = (int)Math.ceil(b.height); + bounds = new Rectangle2D.Double(b.x, b.y, b.width, b.height); + cachedMask = null; + } + + /** Sets the integer boundaries x, y, width, height from given sub-pixel + * boundaries, such that all points are within the integer bounding rectangle. + * For open line selections and (multi)Point Rois, note that integer Roi + * coordinates correspond to the center of the 1x1 rectangle enclosing a pixel. + * Points at the boundary of such a rectangle are counted for the higher x or y + * value, in agreement to how (poly-)line or PointRois are displayed at the + * screen at high zoom levels. (For lines and points, it should include all + * pixels affected by 'draw' */ + void setIntBounds(Rectangle2D.Double bounds) { + if (useLineSubpixelConvention()) { //for PointRois & open lines, ensure the 'draw' area is enclosed + x = (int)Math.floor(bounds.x + 0.5); + y = (int)Math.floor(bounds.y + 0.5); + width = (int)Math.floor(bounds.x + bounds.width + 1.5) - x; + height = (int)Math.floor(bounds.y + bounds.height + 1.5) - y; + } else { //for area Rois, the subpixel bounds must be enclosed in the int bounds + x = (int)Math.floor(bounds.x); + y = (int)Math.floor(bounds.y); + width = (int)Math.ceil(bounds.x + bounds.width) - x; + height = (int)Math.ceil(bounds.y + bounds.height) - y; + } + } + + /** + * @deprecated + * replaced by getBounds() + */ + public Rectangle getBoundingRect() { + return getBounds(); + } + + /** Returns the outline of this selection as a Polygon, or + null if this is a straight line selection. + @see ij.process.ImageProcessor#setRoi + @see ij.process.ImageProcessor#drawPolygon + @see ij.process.ImageProcessor#fillPolygon + */ + public Polygon getPolygon() { + int[] xpoints = new int[4]; + int[] ypoints = new int[4]; + xpoints[0] = x; + ypoints[0] = y; + xpoints[1] = x+width; + ypoints[1] = y; + xpoints[2] = x+width; + ypoints[2] = y+height; + xpoints[3] = x; + ypoints[3] = y+height; + return new Polygon(xpoints, ypoints, 4); + } + + /** Returns the outline (in image pixel coordinates) as a FloatPolygon */ + public FloatPolygon getFloatPolygon() { + if (cornerDiameter>0) { // Rounded Rectangle + ShapeRoi s = new ShapeRoi(this); + return s.getFloatPolygon(); + } else if (subPixelResolution() && bounds!=null) { + float[] xpoints = new float[4]; + float[] ypoints = new float[4]; + xpoints[0] = (float)bounds.x; + ypoints[0] = (float)bounds.y; + xpoints[1] = (float)(bounds.x+bounds.width); + ypoints[1] = (float)bounds.y; + xpoints[2] = (float)(bounds.x+bounds.width); + ypoints[2] = (float)(bounds.y+bounds.height); + xpoints[3] = (float)bounds.x; + ypoints[3] = (float)(bounds.y+bounds.height); + return new FloatPolygon(xpoints, ypoints); + } else { + Polygon p = getPolygon(); + return new FloatPolygon(toFloat(p.xpoints), toFloat(p.ypoints), p.npoints); + } + } + + /** Returns the outline in image pixel coordinates, + * where options may include "close" to add a point to close the outline + * if this is an area roi and the outline is not closed yet. + * (For ShapeRois, "separate" inserts NaN values between subpaths). */ + public FloatPolygon getFloatPolygon(String options) { + options = options.toLowerCase(); + boolean addPointForClose = options.indexOf("close") >= 0; + FloatPolygon fp = getFloatPolygon(); + int n = fp.npoints; + if (isArea() && n > 1) { + boolean isClosed = fp.xpoints[0] == fp.xpoints[n-1] && fp.ypoints[0] == fp.ypoints[n-1]; + if (addPointForClose && !isClosed) + fp.addPoint(fp.xpoints[0], fp.ypoints[0]); + else if (!addPointForClose && isClosed) + fp.npoints--; + } + return fp; + } + + /** Returns, as a FloatPolygon, an interpolated version + * of this selection that has points spaced 1.0 pixel apart. + */ + public FloatPolygon getInterpolatedPolygon() { + return getInterpolatedPolygon(1.0, false); + } + + /** Returns, as a FloatPolygon, an interpolated version of + * this selection with points spaced 'interval' pixels apart. + * If 'smooth' is true, traced and freehand selections are + * first smoothed using a 3 point running average. + */ + public FloatPolygon getInterpolatedPolygon(double interval, boolean smooth) { + FloatPolygon p = (this instanceof Line)?((Line)this).getFloatPoints():getFloatPolygon(); + return getInterpolatedPolygon(p, interval, smooth); + } + + /** + * Returns, as a FloatPolygon, an interpolated version of this selection + * with points spaced abs('interval') pixels apart. If 'smooth' is true, traced + * and freehand selections are first smoothed using a 3 point running + * average. + * If 'interval' is negative, the program is allowed to decrease abs('interval') + * so that the last segment will hit the end point + */ + protected FloatPolygon getInterpolatedPolygon(FloatPolygon p, double interval, boolean smooth) { + boolean allowToAdjust = interval < 0; + interval = Math.abs(interval); + boolean isLine = this.isLine(); + double length = p.getLength(isLine); + + int npoints = p.npoints; + if (npoints<2) + return p; + if (Math.abs(interval)<0.01) { + IJ.error("Interval must be >= 0.01"); + return p; + } + + if (!isLine) {//**append (and later remove) closing point to end of array + npoints++; + p.xpoints = java.util.Arrays.copyOf(p.xpoints, npoints); + p.xpoints[npoints - 1] = p.xpoints[0]; + p.ypoints = java.util.Arrays.copyOf(p.ypoints, npoints); + p.ypoints[npoints - 1] = p.ypoints[0]; + } + int npoints2 = (int) (10 + (length * 1.5) / interval);//allow some headroom + + double tryInterval = interval; + double minDiff = 1e9; + double bestInterval = 0; + int srcPtr = 0;//index of source polygon + int destPtr = 0;//index of destination polygon + double[] destXArr = new double[npoints2]; + double[] destYArr = new double[npoints2]; + int nTrials = 50; + int trial = 0; + while (trial <= nTrials) { + destXArr[0] = p.xpoints[0]; + destYArr[0] = p.ypoints[0]; + srcPtr = 0; + destPtr = 0; + double xA = p.xpoints[0];//start of current segment + double yA = p.ypoints[0]; + + while (srcPtr < npoints - 1) {//collect vertices + double xC = destXArr[destPtr];//center circle + double yC = destYArr[destPtr]; + double xB = p.xpoints[srcPtr + 1];//end of current segment + double yB = p.ypoints[srcPtr + 1]; + double[] intersections = lineCircleIntersection(xA, yA, xB, yB, xC, yC, tryInterval, true); + if (intersections.length >= 2) { + xA = intersections[0];//only use first of two intersections + yA = intersections[1]; + destPtr++; + destXArr[destPtr] = xA; + destYArr[destPtr] = yA; + } else { + srcPtr++;//no intersection found, pick next segment + xA = p.xpoints[srcPtr]; + yA = p.ypoints[srcPtr]; + } + } + destPtr++; + destXArr[destPtr] = p.xpoints[npoints - 1]; + destYArr[destPtr] = p.ypoints[npoints - 1]; + destPtr++; + if (!allowToAdjust) { + if (isLine) + destPtr--; + break; + } + + int nSegments = destPtr - 1; + double dx = destXArr[destPtr - 2] - destXArr[destPtr - 1]; + double dy = destYArr[destPtr - 2] - destYArr[destPtr - 1]; + double lastSeg = Math.sqrt(dx * dx + dy * dy); + + double diff = lastSeg - tryInterval;//always <= 0 + if (Math.abs(diff) < minDiff) { + minDiff = Math.abs(diff); + bestInterval = tryInterval; + } + double feedBackFactor = 0.66;//factor <1: applying soft successive approximation + tryInterval = tryInterval + feedBackFactor * diff / nSegments; + //stop if tryInterval < 80% of interval, OR if last segment differs < 0.05 pixels + if ((tryInterval < 0.8 * interval || Math.abs(diff) < 0.05 || trial == nTrials - 1) && trial < nTrials) { + trial = nTrials;//run one more loop with bestInterval to get best polygon + tryInterval = bestInterval; + } else + trial++; + } + if (!isLine) //**remove closing point from end of array + destPtr--; + float[] xPoints = new float[destPtr]; + float[] yPoints = new float[destPtr]; + for (int jj = 0; jj < destPtr; jj++) { + xPoints[jj] = (float) destXArr[jj]; + yPoints[jj] = (float) destYArr[jj]; + } + FloatPolygon fPoly = new FloatPolygon(xPoints, yPoints); + return fPoly; + } + + /** Returns the coordinates of the pixels inside this ROI as an array of Points. + * @see #getContainedFloatPoints + * @see #iterator + */ + public Point[] getContainedPoints() { + Roi roi = this; + if (isLine()) + roi = convertLineToArea(this); + ImageProcessor mask = roi.getMask(); + Rectangle bounds = roi.getBounds(); + ArrayList points = new ArrayList(); + for (int y=0; y + * Calculates intersections of a line segment with a circle + * Author N.Vischer + * ax, ay, bx, by: points A and B of line segment + * cx, cy, rad: Circle center and radius. + * ignoreOutside: if true, ignores intersections outside the line segment A-B + * Returns an array of 0, 2 or 4 coordinates (for 0, 1, or 2 intersection + * points). If two intersection points are returned, they are listed in travel + * direction A->B + * + */ + public static double[] lineCircleIntersection(double ax, double ay, double bx, double by, double cx, double cy, double rad, boolean ignoreOutside) { + //rotates & translates points A, B and C, creating new points A2, B2 and C2. + //A2 is then on origin, and B2 is on positive x-axis + + double dxAC = cx - ax; + double dyAC = cy - ay; + double lenAC = Math.sqrt(dxAC * dxAC + dyAC * dyAC); + + double dxAB = bx - ax; + double dyAB = by - ay; + + //calculate B2 and C2: + double xB2 = Math.sqrt(dxAB * dxAB + dyAB * dyAB); + + double phi1 = Math.atan2(dyAB, dxAB);//amount of rotation + double phi2 = Math.atan2(dyAC, dxAC); + double phi3 = phi1 - phi2; + double xC2 = lenAC * Math.cos(phi3); + double yC2 = lenAC * Math.sin(phi3);//rotation & translation is done + if (Math.abs(yC2) > rad) + return new double[0];//no intersection found + double halfChord = Math.sqrt(rad * rad - yC2 * yC2); + double sectOne = xC2 - halfChord;//first intersection point, still on x axis + double sectTwo = xC2 + halfChord;//second intersection point, still on x axis + double[] xyCoords = new double[4]; + int ptr = 0; + if ((sectOne >= 0 && sectOne <= xB2) || !ignoreOutside) { + double sectOneX = Math.cos(phi1) * sectOne + ax;//undo rotation and translation + double sectOneY = Math.sin(phi1) * sectOne + ay; + xyCoords[ptr++] = sectOneX; + xyCoords[ptr++] = sectOneY; + } + if ((sectTwo >= 0 && sectTwo <= xB2) || !ignoreOutside) { + double sectTwoX = Math.cos(phi1) * sectTwo + ax;//undo rotation and translation + double sectTwoY = Math.sin(phi1) * sectTwo + ay; + xyCoords[ptr++] = sectTwoX; + xyCoords[ptr++] = sectTwoY; + } + if (halfChord == 0 && ptr > 2) //tangent line returns only one intersection + ptr = 2; + xyCoords = java.util.Arrays.copyOf(xyCoords,ptr); + return xyCoords; + } + + /** Returns a copy of this roi. See Thinking is Java by Bruce Eckel + (www.eckelobjects.com) for a good description of object cloning. */ + public synchronized Object clone() { + try { + Roi r = (Roi)super.clone(); + r.setImage(null); + if (!usingDefaultStroke) + r.setStroke(getStroke()); + r.setFillColor(getFillColor()); + r.imageID = getImageID(); + r.listenersNotified = false; + if (bounds!=null) + r.bounds = (Rectangle2D.Double)bounds.clone(); + return r; + } + catch (CloneNotSupportedException e) {return null;} + } + + /** Aborts constructing or modifying the roi (called by the ImageJ class on escape) */ + public void abortModification(ImagePlus imp) { + if (state == CONSTRUCTING) { + setImage(null); + if (imp!=null) { + Roi savedPreviousRoi = getPreviousRoi(); + imp.setRoi(previousRoi!=null && previousRoi.getImage() == imp ? previousRoi : null); + setPreviousRoi(savedPreviousRoi); //(overrule saving this aborted roi as previousRoi) + } + } else if (state==MOVING) + move(previousSX, previousSY); //move back to starting point + else if (state == MOVING_HANDLE) + moveHandle(previousSX, previousSY); //move handle back to starting point + state = NORMAL; + } + + protected void grow(int sx, int sy) { + if (clipboard!=null) return; + int xNew = ic.offScreenX2(sx); + int yNew = ic.offScreenY2(sy); + if (type==RECTANGLE) { + if (xNew < 0) xNew = 0; + if (yNew < 0) yNew = 0; + } + if (constrain) { + // constrain selection to be square + if (!center) + {growConstrained(xNew, yNew); return;} + int dx, dy, d; + dx = xNew - x; + dy = yNew - y; + if (dx=startX)?startX:startX - width; + y = (yNew>=startY)?startY:startY - height; + if (type==RECTANGLE) { + if ((x+width) > xMax) width = xMax-x; + if ((y+height) > yMax) height = yMax-y; + } + } + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; + oldY = y; + oldWidth = width; + oldHeight = height; + bounds = null; + } + + private void growConstrained(int xNew, int yNew) { + int dx = xNew - startX; + int dy = yNew - startY; + width = height = (int)Math.round(Math.sqrt(dx*dx + dy*dy)); + if (type==RECTANGLE) { + x = (xNew>=startX)?startX:startX - width; + y = (yNew>=startY)?startY:startY - height; + if (x<0) x = 0; + if (y<0) y = 0; + if ((x+width) > xMax) width = xMax-x; + if ((y+height) > yMax) height = yMax-y; + } else { + x = startX + dx/2 - width/2; + y = startY + dy/2 - height/2; + } + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; + oldY = y; + oldWidth = width; + oldHeight = height; + } + + protected void moveHandle(int sx, int sy) { + double asp; + if (clipboard!=null) return; + int ox = ic.offScreenX2(sx); + int oy = ic.offScreenY2(sy); + if (ox<0) ox=0; if (oy<0) oy=0; + if (ox>xMax) ox=xMax; if (oy>yMax) oy=yMax; + int x1=x, y1=y, x2=x1+width, y2=y+height, xc=x+width/2, yc=y+height/2; + if (width > 7 && height > 7) { + asp = (double)width/(double)height; + asp_bk = asp; + } else + asp = asp_bk; + + switch (activeHandle) { + case 0: + x=ox; y=oy; + break; + case 1: + y=oy; + break; + case 2: + x2=ox; y=oy; + break; + case 3: + x2=ox; + break; + case 4: + x2=ox; y2=oy; + break; + case 5: + y2=oy; + break; + case 6: + x=ox; y2=oy; + break; + case 7: + x=ox; + break; + } + if (x=x2) { + width=1; + x=x2=xc; + } + if (y>=y2) { + height=1; + y=y2=yc; + } + bounds = null; + } + + if (constrain) { + if (activeHandle==1 || activeHandle==5) + width=height; + else + height=width; + + if(x>=x2) { + width=1; + x=x2=xc; + } + if (y>=y2) { + height=1; + y=y2=yc; + } + switch (activeHandle) { + case 0: + x=x2-width; + y=y2-height; + break; + case 1: + x=xc-width/2; + y=y2-height; + break; + case 2: + y=y2-height; + break; + case 3: + y=yc-height/2; + break; + case 5: + x=xc-width/2; + break; + case 6: + x=x2-width; + break; + case 7: + y=yc-height/2; + x=x2-width; + break; + } + if (center) { + x=xc-width/2; + y=yc-height/2; + } + } + + if (aspect && !constrain) { + if (activeHandle==1 || activeHandle==5) width=(int)Math.rint((double)height*asp); + else height=(int)Math.rint((double)width/asp); + + switch (activeHandle){ + case 0: + x=x2-width; + y=y2-height; + break; + case 1: + x=xc-width/2; + y=y2-height; + break; + case 2: + y=y2-height; + break; + case 3: + y=yc-height/2; + break; + case 5: + x=xc-width/2; + break; + case 6: + x=x2-width; + break; + case 7: + y=yc-height/2; + x=x2-width; + break; + } + if (center) { + x=xc-width/2; + y=yc-height/2; + } + + // Attempt to preserve aspect ratio when roi very small: + if (width<8) { + if(width<1) width = 1; + height=(int)Math.rint((double)width/asp_bk); + } + if (height<8) { + if(height<1) height =1; + width=(int)Math.rint((double)height*asp_bk); + } + } + + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX=x; oldY=y; + oldWidth=width; oldHeight=height; + bounds = null; + subPixel = false; + } + + void move(int sx, int sy) { + if (constrain) { // constrain translation in 90deg steps + int dx = sx - previousSX; + int dy = sy - previousSY; + if (Math.abs(dx) > Math.abs(dy)) + dy = 0; + else + dx = 0; + sx = previousSX + dx; + sy = previousSY + dy; + } + int xNew = ic.offScreenX(sx); + int yNew = ic.offScreenY(sy); + int dx = xNew - startX; + int dy = yNew - startY; + if (dx==0 && dy==0) + return; + x += dx; + y += dy; + if (bounds!=null) + setLocation(bounds.x + dx, bounds.y + dy); + boolean isImageRoi = this instanceof ImageRoi; + if (clipboard==null && type==RECTANGLE && !isImageRoi) { + if (x<0) x=0; if (y<0) y=0; + if ((x+width)>xMax) x = xMax-width; + if ((y+height)>yMax) y = yMax-height; + } + startX = xNew; + startY = yNew; + if (type==POINT || ((this instanceof TextRoi) && ((TextRoi)this).getAngle()!=0.0)) + ignoreClipRect = true; + updateClipRect(); + if ((lineWidth>1 && isLine()) || ignoreClipRect || ((this instanceof PolygonRoi)&&((PolygonRoi)this).isSplineFit())) + imp.draw(); + else + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; + oldY = y; + oldWidth = width; + oldHeight=height; + if (isImageRoi) showStatus(); + } + + /** Nudge ROI one pixel on arrow key press. */ + public void nudge(int key) { + if (WindowManager.getActiveWindow() instanceof ij.plugin.frame.RoiManager) + return; + if (bounds != null && (!isInteger(bounds.x) || !isInteger(bounds.y))) + cachedMask = null; + switch(key) { + case KeyEvent.VK_UP: + this.y--; + if (this.y<0 && (type!=RECTANGLE||clipboard==null)) + this.y = 0; + break; + case KeyEvent.VK_DOWN: + this.y++; + if ((this.y+height)>=yMax && (type!=RECTANGLE||clipboard==null)) + this.y = yMax-height; + break; + case KeyEvent.VK_LEFT: + this.x--; + if (this.x<0 && (type!=RECTANGLE||clipboard==null)) + this.x = 0; + break; + case KeyEvent.VK_RIGHT: + this.x++; + if ((this.x+width)>=xMax && (type!=RECTANGLE||clipboard==null)) + this.x = xMax-width; + break; + } + updateClipRect(); + if (type==POINT) + imp.draw(); + else + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = this.x; oldY = this.y; + bounds = null; + setLocation(this.x, this.y); + showStatus(); + notifyListeners(RoiListener.MOVED); + } + + /** Nudge lower right corner of rectangular and oval ROIs by + one pixel based on arrow key press. */ + public void nudgeCorner(int key) { + if (type>OVAL || clipboard!=null) + return; + switch(key) { + case KeyEvent.VK_UP: + height--; + if (height<1) height = 1; + notifyListeners(RoiListener.MODIFIED); + break; + case KeyEvent.VK_DOWN: + height++; + if ((y+height) > yMax) height = yMax-y; + notifyListeners(RoiListener.MODIFIED); + break; + case KeyEvent.VK_LEFT: + width--; + if (width<1) width = 1; + notifyListeners(RoiListener.MODIFIED); + break; + case KeyEvent.VK_RIGHT: + width++; + if ((x+width) > xMax) width = xMax-x; + notifyListeners(RoiListener.MODIFIED); + break; + } + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; oldY = y; + cachedMask = null; + showStatus(); + notifyListeners(RoiListener.MOVED); + } + + // Finds the union of current and previous roi + protected void updateClipRect() { + clipX = (x<=oldX)?x:oldX; + clipY = (y<=oldY)?y:oldY; + clipWidth = ((x+width>=oldX+oldWidth)?x+width:oldX+oldWidth) - clipX + 1; + clipHeight = ((y+height>=oldY+oldHeight)?y+height:oldY+oldHeight) - clipY + 1; + int handleSize = getHandleSize(); + double mag = ic!=null?ic.getMagnification():1; + int m = mag<1.0?(int)(handleSize/mag):handleSize; + m += clipRectMargin(); + double strokeWidth = getStrokeWidth(); + if (strokeWidth==0.0) + strokeWidth = defaultStrokeWidth(); + m = (int)(m+strokeWidth*2); + clipX-=m; clipY-=m; + clipWidth+=m*2; clipHeight+=m*2; + } + + protected int clipRectMargin() { + return 0; + } + + protected void handleMouseDrag(int sx, int sy, int flags) { + if (ic==null) return; + constrain = (flags&Event.SHIFT_MASK)!=0; + center = (flags&Event.CTRL_MASK)!=0 || (IJ.isMacintosh()&&(flags&Event.META_MASK)!=0); + aspect = (flags&Event.ALT_MASK)!=0; + switch(state) { + case CONSTRUCTING: + grow(sx, sy); + break; + case MOVING: + move(sx, sy); + break; + case MOVING_HANDLE: + moveHandle(sx, sy); + break; + default: + break; + } + notifyListeners(state==MOVING?RoiListener.MOVED:RoiListener.MODIFIED); + } + + public void draw(Graphics g) { + Color color = strokeColor!=null?strokeColor:ROIColor; + if (fillColor!=null) color = fillColor; + if (Interpreter.isBatchMode() && imp!=null && imp.getOverlay()!=null && strokeColor==null && fillColor==null) + return; + g.setColor(color); + mag = getMagnification(); + int sw = (int)(width*mag); + int sh = (int)(height*mag); + int sx1 = screenX(x); + int sy1 = screenY(y); + if (subPixelResolution() && bounds!=null) { + sw = (int)(bounds.width*mag); + sh = (int)(bounds.height*mag); + sx1 = screenXD(bounds.x); + sy1 = screenYD(bounds.y); + } + int sx2 = sx1+sw/2; + int sy2 = sy1+sh/2; + int sx3 = sx1+sw; + int sy3 = sy1+sh; + Graphics2D g2d = (Graphics2D)g; + if (stroke!=null) + g2d.setStroke(getScaledStroke()); + setRenderingHint(g2d); + if (cornerDiameter>0) { + int sArcSize = (int)Math.round(cornerDiameter*mag); + if (fillColor!=null) + g.fillRoundRect(sx1, sy1, sw, sh, sArcSize, sArcSize); + else + g.drawRoundRect(sx1, sy1, sw, sh, sArcSize, sArcSize); + } else { + if (fillColor!=null) { + if (!overlay && isActiveOverlayRoi()) { + g.setColor(Color.cyan); + g.drawRect(sx1, sy1, sw, sh); + } else { + if (!(this instanceof TextRoi)) + g.fillRect(sx1, sy1, sw, sh); + else + g.drawRect(sx1, sy1, sw, sh); + } + } else + g.drawRect(sx1, sy1, sw, sh); + } + if (clipboard==null && !overlay) { + drawHandle(g, sx1, sy1); + drawHandle(g, sx2, sy1); + drawHandle(g, sx3, sy1); + drawHandle(g, sx3, sy2); + drawHandle(g, sx3, sy3); + drawHandle(g, sx2, sy3); + drawHandle(g, sx1, sy3); + drawHandle(g, sx1, sy2); + } + drawPreviousRoi(g); + if (state!=NORMAL) + showStatus(); + if (updateFullWindow) + {updateFullWindow = false; imp.draw();} + } + + public void drawOverlay(Graphics g) { + overlay = true; + draw(g); + overlay = false; + } + + void drawPreviousRoi(Graphics g) { + if (previousRoi!=null && previousRoi!=this && previousRoi.modState!=NO_MODS) { + if (type!=POINT && previousRoi.getType()==POINT && previousRoi.modState!=SUBTRACT_FROM_ROI) + return; + previousRoi.setImage(imp); + previousRoi.draw(g); + } + } + + private static double defaultStrokeWidth() { + double defaultWidth = defaultStrokeWidth; + double guiScale = Prefs.getGuiScale(); + if (defaultWidth<=1 && guiScale>1.0) { + defaultWidth = guiScale; + if (defaultWidth<1.5) defaultWidth = 1.5; + } + return defaultWidth; + } + + /** Returns the current handle size. */ + public int getHandleSize() { + if (handleSize>=0) + return handleSize; + else + return getDefaultHandleSize(); + } + + /** Sets the current handle size. */ + public void setHandleSize(int size) { + if (size>=0 && ((size&1)==0)) + size++; // add 1 if odd + handleSize = size; + if (imp!=null) + imp.draw(); + } + + /** Returns the default handle size. */ + public static int getDefaultHandleSize() { + if (defaultHandleSize>0) + return defaultHandleSize; + double defaultWidth = defaultStrokeWidth(); + int size = 7; + if (defaultWidth>1.5) size=9; + if (defaultWidth>=3) size=11; + if (defaultWidth>=4) size=13; + if (defaultWidth>=5) size=15; + if (defaultWidth>=11) size=(int)defaultWidth; + defaultHandleSize = size; + return defaultHandleSize; + } + + public static void resetDefaultHandleSize() { + defaultHandleSize = 0; + } + + void drawHandle(Graphics g, int x, int y) { + int threshold1 = 7500; + int threshold2 = 1500; + double size = (this.width*this.height)*this.mag*this.mag; + if (this instanceof Line) { + size = ((Line)this).getLength()*this.mag; + threshold1 = 150; + threshold2 = 50; + } else { + if (state==CONSTRUCTING && !(type==RECTANGLE||type==OVAL)) + size = threshold1 + 1; + } + int width = 7; + int x0=x, y0=y; + if (size>threshold1) { + x -= 3; + y -= 3; + } else if (size>threshold2) { + x -= 2; + y -= 2; + width = 5; + } else { + x--; y--; + width = 3; + } + int inc = getHandleSize() - 7; + width += inc; + x -= inc/2; + y -= inc/2; + g.setColor(Color.black); + if (width<3) { + g.fillRect(x0,y0,1,1); + return; + } + g.fillRect(x++,y++,width,width); + g.setColor(handleColor); + width -= 2; + g.fillRect(x,y,width,width); + } + + /** + * @deprecated + * replaced by drawPixels(ImageProcessor) + */ + public void drawPixels() { + if (imp!=null) + drawPixels(imp.getProcessor()); + } + + /** Draws the selection outline on the specified ImageProcessor. + @see ij.process.ImageProcessor#setColor + @see ij.process.ImageProcessor#setLineWidth + */ + public void drawPixels(ImageProcessor ip) { + endPaste(); + int saveWidth = ip.getLineWidth(); + if (getStrokeWidth()>1f) + ip.setLineWidth((int)Math.round(getStrokeWidth())); + if (cornerDiameter>0) + drawRoundedRect(ip); + else { + if (ip.getLineWidth()==1) + ip.drawRect(x, y, width+1, height+1); + else + ip.drawRect(x, y, width, height); + } + ip.setLineWidth(saveWidth); + if (Line.getWidth()>1 || getStrokeWidth()>1) + updateFullWindow = true; + } + + private void drawRoundedRect(ImageProcessor ip) { + int margin = (int)getStrokeWidth()/2; + BufferedImage bi = new BufferedImage(width+margin*2+1, height+margin*2+1, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g = bi.createGraphics(); + if (stroke!=null) + g.setStroke(stroke); + g.drawRoundRect(margin, margin, width, height, cornerDiameter, cornerDiameter); + ByteProcessor mask = new ByteProcessor(bi); + ip.setRoi(x-margin, y-margin, width+margin*2+1, height+margin*2+1); + ip.fill(mask); + } + + /** Returns whether the center of pixel (x,y) is contained in the Roi. + * The position of a pixel center determines whether a pixel is selected. + * Points exactly at the left (right) border are considered outside (inside); + * points exactly on horizontal borders are considered outside (inside) at the border + * with the lower (higher) y. This convention is opposite to that of the java.awt.Shape class. */ + public boolean contains(int x, int y) { + Rectangle r = new Rectangle(this.x, this.y, width, height); + boolean contains = r.contains(x, y); + if (cornerDiameter==0 || contains==false) + return contains; + RoundRectangle2D rr = new RoundRectangle2D.Double(this.x, this.y, width, height, cornerDiameter, cornerDiameter); + return rr.contains(x+0.4999, y+0.4999); + } + + /** Returns whether coordinate (x,y) is contained in the Roi. + * Note that the coordinate (0,0) is the top-left corner of pixel (0,0). + * Use contains(int, int) to determine whether a given pixel is contained in the Roi. */ + public boolean containsPoint(double x, double y) { + boolean contains = false; + if (bounds == null) + contains = x>=this.x && y>=this.y && x1280?5:3; + int size = getHandleSize()+margin; + int halfSize = size/2; + double x = getXBase(); + double y = getYBase(); + double width = getFloatWidth(); + double height = getFloatHeight(); + int sx1 = screenXD(x) - halfSize; + int sy1 = screenYD(y) - halfSize; + int sx3 = screenXD(x+width) - halfSize; + int sy3 = screenYD(y+height) - halfSize; + int sx2 = sx1 + (sx3 - sx1)/2; + int sy2 = sy1 + (sy3 - sy1)/2; + if (sx>=sx1&&sx<=sx1+size&&sy>=sy1&&sy<=sy1+size) return 0; + if (sx>=sx2&&sx<=sx2+size&&sy>=sy1&&sy<=sy1+size) return 1; + if (sx>=sx3&&sx<=sx3+size&&sy>=sy1&&sy<=sy1+size) return 2; + if (sx>=sx3&&sx<=sx3+size&&sy>=sy2&&sy<=sy2+size) return 3; + if (sx>=sx3&&sx<=sx3+size&&sy>=sy3&&sy<=sy3+size) return 4; + if (sx>=sx2&&sx<=sx2+size&&sy>=sy3&&sy<=sy3+size) return 5; + if (sx>=sx1&&sx<=sx1+size&&sy>=sy3&&sy<=sy3+size) return 6; + if (sx>=sx1&&sx<=sx1+size&&sy>=sy2&&sy<=sy2+size) return 7; + return -1; + } + + protected void mouseDownInHandle(int handle, int sx, int sy) { + state = MOVING_HANDLE; + previousSX = sx; + previousSY = sy; + activeHandle = handle; + } + + protected void handleMouseDown(int sx, int sy) { + if (state==NORMAL && ic!=null) { + state = MOVING; + previousSX = sx; + previousSY = sy; + startX = offScreenX(sx); + startY = offScreenY(sy); + startXD = offScreenXD(sx); + startYD = offScreenYD(sy); + } + } + + protected void handleMouseUp(int screenX, int screenY) { + state = NORMAL; + if (imp==null) return; + imp.draw(clipX-5, clipY-5, clipWidth+10, clipHeight+10); + if (Recorder.record) { + String method; + if (type==OVAL) + Recorder.record("makeOval", x, y, width, height); + else if (!(this instanceof TextRoi)) { + if (cornerDiameter==0) + Recorder.record("makeRectangle", x, y, width, height); + else { + if (Recorder.scriptMode()) + Recorder.recordCall("imp.setRoi(new Roi("+x+","+y+","+width+","+height+","+cornerDiameter+"));"); + else + Recorder.record("makeRectangle", x, y, width, height, cornerDiameter); + } + } + } + if (Toolbar.getToolId()==Toolbar.OVAL&&Toolbar.getBrushSize()>0) { + int flags = ic!=null?ic.getModifiers():16; + if ((flags&16)==0) // erase ROI Brush + {imp.draw(); return;} + } + modifyRoi(); + } + + void modifyRoi() { + if (previousRoi==null || previousRoi.modState==NO_MODS || imp==null) + return; + if (type==POINT || previousRoi.getType()==POINT) { + if (type==POINT && previousRoi.getType()==POINT) { + addPoint(); + } else if (isArea() && previousRoi.getType()==POINT && previousRoi.modState==SUBTRACT_FROM_ROI) + subtractPoints(); + return; + } + Roi previous = (Roi)previousRoi.clone(); + previous.modState = NO_MODS; + ShapeRoi s1 = null; + ShapeRoi s2 = null; + if (previousRoi instanceof ShapeRoi) + s1 = (ShapeRoi)previousRoi; + else + s1 = new ShapeRoi(previousRoi); + if (this instanceof ShapeRoi) + s2 = (ShapeRoi)this; + else + s2 = new ShapeRoi(this); + if (previousRoi.modState==ADD_TO_ROI) + s1.or(s2); + else + s1.not(s2); + previousRoi.modState = NO_MODS; + Roi roi2 = s1.trySimplify(); + if (roi2 == null) return; + if (roi2!=null) + roi2.copyAttributes(previousRoi); + imp.setRoi(roi2); + setPreviousRoi(previous); + } + + void addPoint() { + if (!(type==POINT && previousRoi.getType()==POINT)) { + modState = NO_MODS; + imp.draw(); + return; + } + previousRoi.modState = NO_MODS; + PointRoi p1 = (PointRoi)previousRoi; + FloatPolygon poly = getFloatPolygon(); + p1.addPoint(imp, poly.xpoints[0], poly.ypoints[0]); + imp.setRoi(p1); + } + + void subtractPoints() { + previousRoi.modState = NO_MODS; + PointRoi p1 = (PointRoi)previousRoi; + PointRoi p2 = p1.subtractPoints(this); + if (p2!=null) + imp.setRoi(p1.subtractPoints(this)); + else + imp.deleteRoi(); + } + + /** If 'add' is true, adds this selection to the previous one. If 'subtract' is true, subtracts + it from the previous selection. Called by the IJ.doWand() method, and the makeRectangle(), + makeOval(), makePolygon() and makeSelection() macro functions. */ + public void update(boolean add, boolean subtract) { + if (previousRoi==null) return; + if (add) { + previousRoi.modState = ADD_TO_ROI; + modifyRoi(); + } else if (subtract) { + previousRoi.modState = SUBTRACT_FROM_ROI; + modifyRoi(); + } else + previousRoi.modState = NO_MODS; + } + + public void showStatus() { + if (imp==null) + return; + String value; + if (state!=CONSTRUCTING && (type==RECTANGLE||type==POINT) && width<=25 && height<=25) { + ImageProcessor ip = imp.getProcessor(); + double v = ip.getPixelValue(this.x,this.y); + int digits = (imp.getType()==ImagePlus.GRAY8||imp.getType()==ImagePlus.GRAY16)?0:2; + value = ", value="+IJ.d2s(v,digits); + } else + value = ""; + Calibration cal = imp.getCalibration(); + String size; + if (cal.scaled()) + size = ", w="+IJ.d2s(width*cal.pixelWidth)+" ("+width+"), h="+IJ.d2s(height*cal.pixelHeight)+" ("+height+")"; + else + size = ", w="+width+", h="+height; + IJ.showStatus(imp.getLocationAsString(this.x,this.y)+size+value); + } + + /** Always returns null for rectangular Roi's */ + public ImageProcessor getMask() { + if (cornerDiameter>0) + return (new ShapeRoi(new RoundRectangle2D.Float(x, y, width, height, cornerDiameter, cornerDiameter))).getMask(); + else + return null; + } + + public void startPaste(ImagePlus clipboard) { + IJ.showStatus("Pasting..."); + IJ.wait(10); + this.clipboard = clipboard; + imp.getProcessor().snapshot(); + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + } + + void updatePaste() { + if (clipboard!=null) { + imp.getMask(); + ImageProcessor ip = imp.getProcessor(); + ip.reset(); + int xoffset=0, yoffset=0; + Roi croi = clipboard.getRoi(); + if (croi!=null) { + Rectangle r = croi.getBounds(); + if (r.x<0) xoffset=-r.x; + if (r.y<0) yoffset=-r.y; + } + ip.copyBits(clipboard.getProcessor(), x+xoffset, y+yoffset, pasteMode); + if (type!=RECTANGLE) + ip.reset(ip.getMask()); + if (ic!=null) + ic.setImageUpdated(); + } + } + + public void endPaste() { + if (clipboard!=null) { + updatePaste(); + clipboard = null; + Undo.setup(Undo.FILTER, imp); + } + activeOverlayRoi = false; + } + + public void abortPaste() { + clipboard = null; + imp.getProcessor().reset(); + imp.updateAndDraw(); + } + + /** Returns the default stroke width. */ + public static double getDefaultStrokeWidth() { + return defaultStrokeWidth; + } + + /** Sets the default stroke width. */ + public static void setDefaultStrokeWidth(double width) { + defaultStrokeWidth = width<0.0?0.0:width; + resetDefaultHandleSize(); + } + + /** Returns the group value assigned to newly created ROIs. */ + public static int getDefaultGroup() { + return defaultGroup; + } + + /** Sets the group value assigned to newly created ROIs, and also + * sets the default ROI color to the group color. Set to zero to not + * have a default group and to use the default ROI color. + * @see #setGroup + * @see #getGroup + * @see #getGroupColor + */ + public static void setDefaultGroup(int group) { + if (group<0 || group>255) + throw new IllegalArgumentException("Invalid group: "+group); + defaultGroup = group; + groupColor = getGroupColor(group); + } + + /** Returns the group attribute of this ROI. */ + public int getGroup() { + return this.group; + } + + /** Returns the group name associtated with the specified group. */ + public static String getGroupName(int groupNumber) { + if (groupNumber<1 || groupNumber>255) + return null; + if (groupNames==null && groupNamesString==null) + return null; + if (groupNames==null) + groupNames = groupNamesString.split(","); + if (groupNumber>groupNames.length) + return null; + String name = groupNames[groupNumber-1]; + if (name==null) + return null; + return name.length()>0?name:null; + } + + public static synchronized void setGroupName(int groupNumber, String name) { + if (groupNumber<1 || groupNumber>255) + return; + if (groupNamesString==null && groupNames==null) + groupNames = new String[groupNumber]; + if (groupNames==null) + groupNames = groupNamesString.split(","); + if (groupNumber>groupNames.length) { + String[] temp = new String[groupNumber]; + for (int i=0; i255) + throw new IllegalArgumentException("Invalid group: "+group); + if (group>0) + setStrokeColor(getGroupColor(group)); + if (group==0 && this.group>0) + setStrokeColor(null); + this.group = group; + if (imp!=null) // Update Roi Color in the GUI + imp.draw(); + } + + /** Retrieves color associated to a given roi group. */ + private static Color getGroupColor(int group) { + Color color = ROIColor; // default ROI color + if (group>0) { // read Glasbey Lut + if (glasbeyLut==null) { + String path = IJ.getDir("luts")+"Glasbey.lut"; + glasbeyLut = LutLoader.openLut("noerror:"+path); + if (glasbeyLut==null) + IJ.log("LUT not found: "+path); + } + if (glasbeyLut!=null) + color = new Color(glasbeyLut.getRGB(group)); + } + return color; + } + + /** Returns the angle in degrees between the specified line and a horizontal line. */ + public double getAngle(int x1, int y1, int x2, int y2) { + return getFloatAngle(x1, y1, x2, y2); + } + + /** Returns the angle in degrees between the specified line and a horizontal line. */ + public double getFloatAngle(double x1, double y1, double x2, double y2) { + double dx = x2-x1; + double dy = y1-y2; + if (imp!=null && !IJ.altKeyDown()) { + Calibration cal = imp.getCalibration(); + dx *= cal.pixelWidth; + dy *= cal.pixelHeight; + } + return (180.0/Math.PI)*Math.atan2(dy, dx); + } + + /** Sets the default (global) color used for ROI outlines. + * @see #getColor() + * @see #setStrokeColor(Color) + */ + public static void setColor(Color c) { + ROIColor = c; + } + + /** Returns the default (global) color used for drawing ROI outlines. + * @see #setColor(Color) + * @see #getStrokeColor() + */ + public static Color getColor() { + return ROIColor; + } + + /** Sets the color used by this ROI to draw its outline. This color, if not null, + * overrides the global color set by the static setColor() method. + * @see #getStrokeColor + * @see #setStrokeWidth + * @see ij.ImagePlus#setOverlay(ij.gui.Overlay) + */ + public void setStrokeColor(Color c) { + strokeColor = c; + } + + /** Returns the the color used to draw the ROI outline or null if the default color is being used. + * @see #setStrokeColor(Color) + */ + public Color getStrokeColor() { + return strokeColor; + } + + /** Sets the default stroke color. */ + public static void setDefaultColor(Color color) { + defaultColor = color; + } + + /** Sets the fill color used to display this ROI, or set to null to display it transparently. + * @see #getFillColor + * @see #setStrokeColor + */ + public void setFillColor(Color color) { + fillColor = color; + } + + /** Returns the fill color used to display this ROI, or null if it is displayed transparently. + * @see #setFillColor + * @see #getStrokeColor + */ + public Color getFillColor() { + return fillColor; + } + + public static void setDefaultFillColor(Color color) { + defaultFillColor = color; + } + + public static Color getDefaultFillColor() { + return defaultFillColor; + } + + public void setAntiAlias(boolean antiAlias) { + this.antiAlias = antiAlias; + } + + public boolean getAntiAlias() { + return antiAlias; + } + + protected void setRenderingHint(Graphics2D g2d) { + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + antiAlias?RenderingHints.VALUE_ANTIALIAS_ON:RenderingHints.VALUE_ANTIALIAS_OFF); + } + + /** Copy the attributes (outline color, fill color, outline width) + of 'roi2' to the this selection. */ + public void copyAttributes(Roi roi2) { + this. strokeColor = roi2. strokeColor; + this.fillColor = roi2.fillColor; + this.setStrokeWidth(roi2.getStrokeWidth()); + this.setName(roi2.getName()); + this.group = roi2.group; + } + + /** + * @deprecated + * replaced by setStrokeColor() + */ + public void setInstanceColor(Color c) { + strokeColor = c; + } + + /** + * @deprecated + * replaced by setStrokeWidth(int) + */ + public void setLineWidth(int width) { + setStrokeWidth(width) ; + } + + public void updateWideLine(float width) { + if (isLine()) { + wideLine = true; + setStrokeWidth(width); + if (getStrokeColor()==null) { + Color c = getColor(); + setStrokeColor(new Color(c.getRed(),c.getGreen(),c.getBlue(), 77)); + } + } + } + + /** Set 'nonScalable' true to have TextRois in a display + list drawn at a fixed location and size. */ + public void setNonScalable(boolean nonScalable) { + this.nonScalable = nonScalable; + } + + /** Sets the width of the line used to draw this ROI. Set + * the width to 0.0 and the ROI will be drawn using a + * a 1 pixel stroke width regardless of the magnification. + * @see #setDefaultStrokeWidth(double) + * @see #setStrokeColor(Color) + * @see ij.ImagePlus#setOverlay(ij.gui.Overlay) + */ + public void setStrokeWidth(float strokeWidth) { + if (strokeWidth<0f) + strokeWidth = 0f; + if (strokeWidth==0f && usingDefaultStroke) + return; + if (strokeWidth>0f) { + scaleStrokeWidth = true; + usingDefaultStroke = false; + } + boolean notify = listeners.size()>0 && isLine() && getStrokeWidth()!=strokeWidth; + if (strokeWidth==0f) + this.stroke = null; + else if (wideLine) + this.stroke = new BasicStroke(strokeWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); + else + this.stroke = new BasicStroke(strokeWidth); + if (strokeWidth>1f) + fillColor = null; + if (notify) + notifyListeners(RoiListener.MODIFIED); + } + + /** This is a version of setStrokeWidth() that accepts a double argument. */ + public void setStrokeWidth(double strokeWidth) { + setStrokeWidth((float)strokeWidth); + } + + public void setUnscalableStrokeWidth(double strokeWidth) { + setStrokeWidth((float)strokeWidth); + scaleStrokeWidth = false; + + } + + /** Returns the lineWidth. */ + public float getStrokeWidth() { + return (stroke!=null&&!usingDefaultStroke)?stroke.getLineWidth():0f; + } + + /** Sets the Stroke used to draw this ROI. */ + public void setStroke(BasicStroke stroke) { + this.stroke = stroke; + if (stroke!=null) + usingDefaultStroke = false; + } + + /** Returns the Stroke used to draw this ROI, or null if no Stroke is used. */ + public BasicStroke getStroke() { + if (usingDefaultStroke) + return null; + else + return stroke; + } + + /** Returns 'true' if the stroke width is scaled as images are zoomed. */ + public boolean getScaleStrokeWidth() { + return scaleStrokeWidth; + } + + protected BasicStroke getScaledStroke() { + if (ic==null || usingDefaultStroke || !scaleStrokeWidth) + return stroke; + double mag = ic.getMagnification(); + if (mag!=1.0) { + float width = (float)(stroke.getLineWidth()*mag); + //return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); + return new BasicStroke(width, stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(), stroke.getDashArray(), stroke.getDashPhase()); + } else + return stroke; + } + + /** Returns the name of this ROI, or null. */ + public String getName() { + return name; + } + + /** Sets the name of this ROI. */ + public void setName(String name) { + this.name = name; + } + + /** Sets the Paste transfer mode. + @see ij.process.Blitter + */ + public static void setPasteMode(int transferMode) { + if (transferMode==pasteMode) return; + pasteMode = transferMode; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) + imp.updateAndDraw(); + } + + /** Sets the rounded rectangle corner diameter (pixels). */ + public void setCornerDiameter(int cornerDiameter) { + if (cornerDiameter<0) cornerDiameter = 0; + this.cornerDiameter = cornerDiameter; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null && this==imp.getRoi()) + imp.updateAndDraw(); + } + + /** Returns the rounded rectangle corner diameter (pixels). */ + public int getCornerDiameter() { + return cornerDiameter; + } + + /** Obsolete; replaced by setCornerDiameter(). */ + public void setRoundRectArcSize(int cornerDiameter) { + setCornerDiameter(cornerDiameter); + } + + /** Obsolete; replaced by getCornerDiameter(). */ + public int getRoundRectArcSize() { + return cornerDiameter; + } + + /** Sets the stack position (image number) of this ROI. In an overlay, this + * ROI is only displayed when the stack is at the specified position. + * Set to zero to have the ROI displayed on all images in the stack. + * @see ij.gui.Overlay + */ + public void setPosition(int n) { + if (n<0) n=0; + position = n; + channel = slice = frame = 0; + hyperstackPosition = false; + } + + /** Returns the stack position (image number) of this ROI, or + * zero if the ROI is not associated with a particular stack image. + * @see ij.gui.Overlay + */ + public int getPosition() { + return position; + } + + /** Sets the hyperstack position of this ROI. In an overlay, this + * ROI is only displayed when the hyperstack is at the specified position. + * @see ij.gui.Overlay + */ + public void setPosition(int channel, int slice, int frame) { + if (channel<0) channel=0; + this.channel = channel; + if (slice<0) slice=0; + this.slice = slice; + if (frame<0) frame=0; + this.frame = frame; + position = 0; + hyperstackPosition = true; + } + + /** Returns 'true' if setPosition(C,Z,T) has been called. */ + public boolean hasHyperStackPosition() { + return hyperstackPosition; + } + + /** Sets the position of this ROI based on the stack position of the specified image. */ + public void setPosition(ImagePlus imp ) { + if (imp==null) + return; + if (imp.isHyperStack()) { + int channel = imp.getDisplayMode()==IJ.COMPOSITE?0:imp.getChannel(); + setPosition(channel, imp.getSlice(), imp.getFrame()); + } else if (imp.getStackSize()>1) + setPosition(imp.getCurrentSlice()); + else + setPosition(0); + } + + /** Returns the channel position of this ROI, or zero + * if this ROI is not associated with a particular channel. + */ + public final int getCPosition() { + return channel; + } + + /** Returns the slice position of this ROI, or zero + * if this ROI is not associated with a particular slice. + */ + public final int getZPosition() { + return slice==0&&!hyperstackPosition?position:slice; + } + + /** Returns the frame position of this ROI, or zero + * if this ROI is not associated with a particular frame. + */ + public final int getTPosition() { + return frame; + } + + // Used by the FileSaver and RoiEncoder to save overlay settings. */ + public void setPrototypeOverlay(Overlay overlay) { + prototypeOverlay = new Overlay(); + prototypeOverlay.drawLabels(overlay.getDrawLabels()); + prototypeOverlay.drawNames(overlay.getDrawNames()); + prototypeOverlay.drawBackgrounds(overlay.getDrawBackgrounds()); + prototypeOverlay.setLabelColor(overlay.getLabelColor()); + prototypeOverlay.setLabelFont(overlay.getLabelFont(), overlay.scalableLabels()); + } + + // Used by the FileOpener and RoiDecoder to restore overlay settings. */ + public Overlay getPrototypeOverlay() { + if (prototypeOverlay!=null) + return prototypeOverlay; + else + return new Overlay(); + } + + /** Returns the current paste transfer mode, or NOT_PASTING (-1) + if no paste operation is in progress. + @see ij.process.Blitter + */ + public int getPasteMode() { + if (clipboard==null) + return NOT_PASTING; + else + return pasteMode; + } + + /** Returns the current paste transfer mode. */ + public static int getCurrentPasteMode() { + return pasteMode; + } + + /** Returns 'true' if this is an area selection. */ + public boolean isArea() { + return (type>=RECTANGLE && type<=TRACED_ROI) || type==COMPOSITE; + } + + /** Returns 'true' if this is a line selection. */ + public boolean isLine() { + return type>=LINE && type<=FREELINE; + } + + + /** Return 'true' if this is a line or point selection. */ + protected boolean isLineOrPoint() { + return isLine() || type==POINT; + } + + /** Returns 'true' if this is an ROI primarily used from drawing + (e.g., TextRoi or Arrow). */ + public boolean isDrawingTool() { + //return cornerDiameter>0; + return false; + } + + protected double getMagnification() { + return ic!=null?ic.getMagnification():1.0; + } + + /** Convenience method that converts Roi type to a human-readable form. */ + public String getTypeAsString() { + String s=""; + switch(type) { + case POLYGON: s="Polygon"; + if (this instanceof EllipseRoi) s="Ellipse"; + if (this instanceof RotatedRectRoi) s="Rotated Rectangle"; + break; + case FREEROI: s="Freehand"; break; + case TRACED_ROI: s="Traced"; break; + case POLYLINE: s="Polyline"; break; + case FREELINE: s="Freeline"; break; + case ANGLE: s="Angle"; break; + case LINE: s=this instanceof Arrow ? "Arrow" : "Straight Line"; break; + case OVAL: s="Oval"; break; + case COMPOSITE: s = "Composite"; break; + case POINT: s="Point"; break; + default: + if (this instanceof TextRoi) + s = "Text"; + else if (this instanceof ImageRoi) + s = "Image"; + else + s = "Rectangle"; + break; + } + return s; + } + + /** Returns true if this ROI is currently displayed on an image. */ + public boolean isVisible() { + return ic!=null; + } + + /** Returns true if this is a slection that supports sub-pixel resolution. */ + public boolean subPixelResolution() { + return subPixel; + } + + /** @deprecated Drawoffset is not used any more. */ + @Deprecated + public boolean getDrawOffset() { + return false; + } + + /** @deprecated This method was previously used to draw lines and polylines shifted + * by 0.5 pixels top the bottom and right, for better agreement with the + * position used by ProfilePlot, with the default taken from + * Prefs.subPixelResolution. Now the shift is independent of this + * setting and only depends on the ROI type (area or line/point ROI). */ + @Deprecated + public void setDrawOffset(boolean drawOffset) { + } + + public void setIgnoreClipRect(boolean ignoreClipRect) { + this.ignoreClipRect = ignoreClipRect; + } + + /** Returns 'true' if this ROI is displayed and is also in an overlay. */ + public final boolean isActiveOverlayRoi() { + if (imp==null || this!=imp.getRoi()) + return false; + Overlay overlay = imp.getOverlay(); + if (overlay!=null && overlay.contains(this)) + return true; + ImageCanvas ic = imp.getCanvas(); + overlay = ic!=null?ic.getShowAllList():null; // ROI Manager overlay + return overlay!=null && overlay.contains(this); + } + + /** Checks whether two rectangles are equal. */ + public boolean equals(Object obj) { + if (obj instanceof Roi) { + Roi roi2 = (Roi)obj; + if (type!=roi2.getType()) return false; + if (!getBounds().equals(roi2.getBounds())) return false; + if (getLength()!=roi2.getLength()) return false; + return true; + } else + return false; + } + + /** Converts image canvas screen x coordinates to integer offscreen image pixel + * coordinates, depending on whether this roi uses the line or area convention + * for coordinates. */ + protected int offScreenX(int sx) { + if (ic == null) return sx; + return useLineSubpixelConvention() ? ic.offScreenX(sx) : ic.offScreenX2(sx); + } + + /** Converts image canvas screen y coordinates to integer offscreen image pixel + * coordinates, depending on whether this roi uses the line or area convention + * for coordinates. */ + protected int offScreenY(int sy) { + if (ic == null) return sy; + return useLineSubpixelConvention() ? ic.offScreenY(sy) : ic.offScreenY2(sy); + } + + /** Converts image canvas screen x coordinates to floating-point offscreen image pixel + * coordinates, depending on whether this roi uses the line or area convention + * for coordinates. */ + protected double offScreenXD(int sx) { + if (ic == null) return sx; + double offScreenValue = ic.offScreenXD(sx); + if (useLineSubpixelConvention()) + offScreenValue -= 0.5; + return offScreenValue; + } + + /** Converts image canvas screen y coordinates to floating-point offscreen image pixel + * coordinates, depending on whether this roi uses the line or area convention + * for coordinates. */ + protected double offScreenYD(int sy) { + if (ic == null) return sy; + double offScreenValue = ic.offScreenYD(sy); + if (useLineSubpixelConvention()) + offScreenValue -= 0.5; + return offScreenValue; + } + + /** Returns 'true' if this ROI uses for drawing the convention for + * line and point ROIs, where the coordinates are with respect + * to the pixel center. + * Returns false for area rois, which have coordinates with respect to + * the upper left corners of the pixels */ + protected boolean useLineSubpixelConvention() { + return isLineOrPoint(); + } + + /** Returns whether a roi created interactively should have subpixel resolution, + * (if the roi type supports it), i.e., whether the magnification is high enough */ + protected boolean magnificationForSubPixel() { + return magnificationForSubPixel(getMagnification()); + } + + protected static boolean magnificationForSubPixel(double magnification) { + return magnification > 1.5; + } + + /**Converts an image pixel x (offscreen)coordinate to a screen x coordinate, + * taking the the line or area convention for coordinates into account */ + protected int screenXD(double ox) { + if (ic == null) return (int)ox; + if (useLineSubpixelConvention()) ox += 0.5; + return ic.screenXD(ox); + } + + /**Converts an image pixel y (offscreen)coordinate to a screen y coordinate, + * taking the the line or area convention for coordinates into account */ + protected int screenYD(double oy) { + if (ic == null) return (int)oy; + if (useLineSubpixelConvention()) oy += 0.5; + return ic.screenYD(oy); + } + + protected int screenX(int ox) {return screenXD(ox);} + protected int screenY(int oy) {return screenYD(oy);} + + /** Converts a float array to an int array using truncation. */ + public static int[] toInt(float[] arr) { + return toInt(arr, null, arr.length); + } + + public static int[] toInt(float[] arr, int[] arr2, int size) { + int n = arr.length; + if (size>n) size=n; + int[] temp = arr2; + if (temp==null || temp.length + * Author: Peter Haub (phaub at dipsystems.de) + */ + public double[] getContourCentroid() { + double xC=0, yC=0, lSum=0, x, y, dx, dy, l; + FloatPolygon poly = getFloatPolygon(); + int nPoints = poly.npoints; + int n2 = nPoints-1; + for (int n1=0; n1 + * Author: Michael Schmid + */ + public static Roi convertLineToArea(Roi line) { + if (line==null || !line.isLine()) + throw new IllegalArgumentException("Line selection required"); + double lineWidth = line.getStrokeWidth(); + Roi roi2 = null; + if (line.getType()==Roi.LINE) { + if (lineWidth<=1.0) + lineWidth = 1.0000001; + FloatPolygon p = ((Line)line).getFloatPolygon(lineWidth); + roi2 = new PolygonRoi(p, Roi.POLYGON); + line.setStrokeWidth(lineWidth); + } else { + if (lineWidth<1) + lineWidth = 1; + Rectangle bounds = line.getBounds(); + double width = bounds.x+bounds.width + lineWidth; + double height = bounds.y+bounds.height + lineWidth; + ByteProcessor ip = new ByteProcessor((int)Math.round(width), (int)Math.round(height)); + PolygonFiller polygonFiller = new PolygonFiller(); + //ip.setColor(255); + double radius = lineWidth/2.0; + FloatPolygon p = line.getFloatPolygon(); + int n = p.npoints; + float[] xv = new float[4]; //vertex points of rectangle will be filled for each line segment + float[] yv = new float[4]; + float[] xt = new float[3]; //vertex points of triangle will be filled between line segments + float[] yt = new float[3]; + double dx1 = p.xpoints[1]-p.xpoints[0]; + double dy1 = p.ypoints[1]-p.ypoints[0]; + double l = length(dx1, dy1); + dx1 = dx1/l; //unit vector along current line segment + dy1 = dy1/l; + double dx0 = dx1; + double dy0 = dy1; + double xfrom = p.xpoints[0] - 0.5*dx1; + double yfrom = p.ypoints[0] - 0.5*dy1; + //Overlay ovly = new Overlay(); + for (int i=1; i1) { //fill triangle to previous line segment + boolean rightTurn=(dx1*dy0>dx0*dy1); + xt[0] = (float)xfrom; + yt[0] = (float)yfrom; + if (rightTurn) { + xt[1] = (float)(xfrom-radius*dy0); + yt[1] = (float)(yfrom+radius*dx0); + xt[2] = (float)(xfrom-radius*dy1); + yt[2] = (float)(yfrom+radius*dx1); + xt[0] += (float)(0.5*(radius*dy0+radius*dy1)); //extend triangle to avoid missing pixels (due to rounding errors) + yt[0] -= (float)(0.5*(radius*dx0+radius*dx1)); //where it touches a rectangle + } else { + xt[1] = (float)(xfrom+radius*dy0); + yt[1] = (float)(yfrom-radius*dx0); + xt[2] = (float)(xfrom+radius*dy1); + yt[2] = (float)(yfrom-radius*dx1); + xt[0] -= (float)(0.5*(radius*dy0+radius*dy1)); + yt[0] += (float)(0.5*(radius*dx0+radius*dx1)); + } + polygonFiller.setPolygon(xt, yt, 3, 0.5f, 0.5f); + polygonFiller.fillByteProcessorMask(ip); + //ovly.add(new PolygonRoi(xt,yt,Roi.POLYGON)); + } + dx0 = dx1; + dy0 = dy1; + xfrom = xto; + yfrom = yto; + if (i + * for (Point p : roi) { + * // process p + * } + * + * Author: Wilhelm Burger + * @see #getContainedPoints() + * @see #getContainedFloatPoints() + */ + public Iterator iterator() { + // Returns the default (mask-based) point iterator. Note that 'Line' overrides the + // iterator() method and returns a specific point iterator. + return new RoiPointsIteratorMask(); + } + + + /** + * Default iterator over points contained in a mask-backed {@link Roi}. + * Author: W. Burger + */ + private class RoiPointsIteratorMask implements Iterator { + private ImageProcessor mask; + private final Rectangle bounds; + private final int xbase, ybase; + private final int n; + private int next; + + RoiPointsIteratorMask() { + if (isLine()) { + Roi roi2 = Roi.convertLineToArea(Roi.this); + mask = roi2.getMask(); + xbase = roi2.x; + ybase = roi2.y; + } else { + mask = getMask(); + if (mask==null && type==RECTANGLE) { + mask = new ByteProcessor(width, height); + mask.invert(); + } + xbase = Roi.this.x; + ybase = Roi.this.y; + } + bounds = new Rectangle(mask.getWidth(), mask.getHeight()); + n = bounds.width * bounds.height; + findNext(0); // sets next + } + + @Override + public boolean hasNext() { + return next < n; + } + + @Override + public Point next() { + if (next >= n) + throw new NoSuchElementException(); + int x = next % bounds.width; + int y = next / bounds.width; + findNext(next+1); + return new Point(xbase+x, ybase+y); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + // finds the next element (from start), sets next + private void findNext(int start) { + if (mask == null) + next = start; + else { + next = n; + for (int i=start; i0", null, Color.gray); + gd.addDialogListener(this); + gd.showDialog(); + if (gd.wasCanceled()) { + Roi.setGroupNames(groupNames); + Roi.setColor(color); + Roi.setDefaultStrokeWidth(strokeWidth); + Roi.setDefaultGroup(group); + return; + } + if (nameChanges) + Roi.saveGroupNames(); + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + int currentGroup = Roi.getDefaultGroup(); + String cname = gd.getNextChoice(); + Color color = Colors.getColor(cname, Color.yellow); + Roi.setColor(color); + if (color.equals(Roi.getColor())) { + Toolbar.repaintTool(Toolbar.POINT); + Toolbar.repaintTool(Toolbar.WAND); + } + Roi.setDefaultStrokeWidth(gd.getNextNumber()); + int group = (int)gd.getNextNumber(); + Vector stringFields = gd.getStringFields(); + TextField nameField = (TextField)(stringFields.get(0)); + if (group>=0 && group<=255 && group!=currentGroup) { + Roi.setDefaultGroup(group); + String name = getGroupName(group); + nameField.setText(name); + } else { + String name = getGroupName(group); + String name2 = nameField.getText(); + if (name2!=null && !name2.equals(name)) { + Roi.setGroupName(group, name2); + nameChanges = true; + } + } + return true; + } + + private String getGroupName(int group) { + String gname = Roi.getGroupName(group); + if (group==0) + gname = "0 = no group"; + else if (gname==null) + gname = "unnamed"; + return gname; + } + +} diff --git a/src/ij/gui/RoiListener.java b/src/ij/gui/RoiListener.java new file mode 100644 index 0000000..5e847eb --- /dev/null +++ b/src/ij/gui/RoiListener.java @@ -0,0 +1,18 @@ +package ij.gui; +import ij.ImagePlus; + + /** Plugins that implement this interface are notified when + an ROI is created, modified or deleted. The + Plugins/Utilities/Monitor Events command uses this interface. + */ + public interface RoiListener { + public static final int CREATED = 1; + public static final int MOVED = 2; + public static final int MODIFIED = 3; + public static final int EXTENDED = 4; + public static final int COMPLETED = 5; + public static final int DELETED = 6; + + public void roiModified(ImagePlus imp, int id); + +} diff --git a/src/ij/gui/RoiProperties.java b/src/ij/gui/RoiProperties.java new file mode 100644 index 0000000..71cabe8 --- /dev/null +++ b/src/ij/gui/RoiProperties.java @@ -0,0 +1,428 @@ +package ij.gui; +import ij.*; +import ij.plugin.Colors; +import ij.io.RoiDecoder; +import ij.process.*; +import ij.measure.*; +import ij.util.Tools; +import ij.plugin.filter.Analyzer; +import ij.text.TextWindow; +import java.awt.*; +import java.util.*; +import java.awt.event.*; + + + /** Displays a dialog that allows the user to specify ROI properties such as color and line width. */ +public class RoiProperties implements TextListener, WindowListener { + private ImagePlus imp; + private Roi roi; + private Overlay overlay; + private String title; + private boolean showName = true; + private boolean showListCoordinates; + private boolean addToOverlay; + private boolean overlayOptions; + private boolean setPositions; + private boolean listCoordinates; + private boolean listProperties; + private boolean showPointCounts; + private static final String[] justNames = {"Left", "Center", "Right"}; + private int nProperties; + private TextField groupField, colorField; + private Label groupName; + + /** Constructs a ColorChooser using the specified title and initial color. */ + public RoiProperties(String title, Roi roi) { + if (roi==null) + throw new IllegalArgumentException("ROI is null"); + this.title = title; + showName = title.startsWith("Prop"); + showListCoordinates = showName && title.endsWith(" "); + nProperties = showListCoordinates?roi.getPropertyCount():0; + addToOverlay = title.equals("Add to Overlay"); + overlayOptions = title.equals("Overlay Options"); + if (overlayOptions) { + imp = WindowManager.getCurrentImage(); + overlay = imp!=null?imp.getOverlay():null; + setPositions = roi.getPosition()!=0; + } + this.roi = roi; + } + + /** Displays the dialog box and returns 'false' if the user cancels it. */ + public boolean showDialog() { + String name= roi.getName(); + boolean isRange = name!=null && name.startsWith("range:"); + String nameLabel = isRange?"Range:":"Name:"; + if (isRange) name = name.substring(7); + if (name==null) name = ""; + if (!isRange && (roi instanceof ImageRoi) && !overlayOptions) + return showImageDialog(name); + Color strokeColor = roi.getStrokeColor(); + Color fillColor = roi.getFillColor(); + double strokeWidth = roi.getStrokeWidth(); + double strokeWidth2 = strokeWidth; + boolean isText = roi instanceof TextRoi; + boolean isLine = roi.isLine(); + boolean isPoint = roi instanceof PointRoi; + int justification = TextRoi.LEFT; + double angle = 0.0; + boolean antialias = true; + if (isText) { + TextRoi troi = (TextRoi)roi; + Font font = troi.getCurrentFont(); + strokeWidth = font.getSize(); + angle = troi.getAngle(); + justification = troi.getJustification(); + antialias = troi.getAntiAlias(); + } + String position = ""+roi.getPosition(); + if (roi.hasHyperStackPosition()) + position = roi.getCPosition() +","+roi.getZPosition()+","+ roi.getTPosition(); + if (position.equals("0")) + position = "none"; + String group = ""+roi.getGroup(); + if (group.equals("0")) + group = "none"; + String linec = Colors.colorToString(strokeColor); + String fillc = Colors.colorToString(fillColor); + if (IJ.isMacro()) { + fillc = "none"; + setPositions = false; + } + int digits = (int)strokeWidth==strokeWidth?0:1; + GenericDialog gd = new GenericDialog(title); + if (showName) { + gd.addStringField(nameLabel, name, 20); + String label = "Position:"; + ImagePlus imp = WindowManager.getCurrentImage(); + if (position.contains(",") || (imp!=null&&imp.isHyperStack())) + label = "Position (c,s,f):"; + gd.addStringField(label, position); + gd.addStringField("Group:", group); + gd.addToSameRow(); gd.addMessage("wwwwwwwwwwww"); + } + if (isText) { + gd.addStringField("Stroke color:", linec); + gd.addNumericField("Font size:", strokeWidth, digits, 4, "points"); + digits = (int)angle==angle?0:1; + gd.addNumericField("Angle:", angle, digits, 4, "degrees"); + gd.setInsets(0, 0, 0); + gd.addChoice("Justification:", justNames, justNames[justification]); + } else { + if (isPoint) + gd.addStringField("Stroke (point) color:", linec); + else { + gd.addStringField("Stroke color:", linec); + gd.addNumericField("Width:", strokeWidth, digits); + } + } + groupName = (Label)gd.getMessage(); + if (showName && !IJ.isMacro()) { + Vector v = gd.getStringFields(); + groupField = (TextField)v.elementAt(v.size()-2); + groupField.addTextListener(this); + colorField = (TextField)v.elementAt(v.size()-1); + } + + + if (!isLine) { + if (isPoint) { + int index = ((PointRoi)roi).getPointType(); + gd.addChoice("Point type:", PointRoi.types, PointRoi.types[index]); + index = ((PointRoi)roi).getSize(); + gd.addChoice("Size:", PointRoi.sizes, PointRoi.sizes[index]); + } else { + gd.addMessage(""); + gd.addStringField("Fill color:", fillc); + } + } + if (addToOverlay) + gd.addCheckbox("New overlay", false); + if (overlayOptions) { + gd.addCheckbox("Set stack positions", setPositions); + if (overlay!=null) { + int size = overlay.size(); + gd.setInsets(15,20,0); + if (imp!=null && imp.getHideOverlay()) + gd.addMessage("Current overlay is hidden", null, Color.darkGray); + else + gd.addMessage("Current overlay has "+size+" element"+(size>1?"s":""), null, Color.darkGray); + gd.setInsets(0,30,0); + gd.addCheckbox("Apply", false); + gd.setInsets(0,30,0); + gd.addCheckbox("Show labels", overlay.getDrawLabels()); + gd.setInsets(0,30,0); + gd.addCheckbox("Hide", imp!=null?imp.getHideOverlay():false); + } else + gd.addMessage("No overlay", null, Color.darkGray); + } + if (isText) + gd.addCheckbox("Antialiased text", antialias); + if (showListCoordinates) { + if ((roi instanceof PointRoi) && Toolbar.getMultiPointMode()) + showPointCounts = true; + if (showPointCounts) + gd.addCheckbox("Show point counts (shortcut: alt+y)", listCoordinates); + else + gd.addCheckbox("List coordinates ("+roi.size()+")", listCoordinates); + if (nProperties>0) + gd.addCheckbox("List properties ("+nProperties+")", listProperties); + else { + gd.setInsets(5,20,0); + gd.addMessage("No properties"); + } + } + if (isText && !isRange) { + String text = ((TextRoi)roi).getText(); + int nLines = Tools.split(text, "\n").length + 1; + gd.addTextAreas(text, null, Math.min(nLines+1, 5), 30); + } + if (showName && "".equals(name) && "none".equals(position) && "none".equals(group) && "none".equals(fillc)) + gd.setSmartRecording(true); + gd.addWindowListener(this); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + String position2 = ""; + String group2 = ""; + if (showName) { + name = gd.getNextString(); + if (!isRange) roi.setName(name.length()>0?name:null); + position2 = gd.getNextString(); + group2 = gd.getNextString(); + } + linec = gd.getNextString(); + if (!isPoint) + strokeWidth2 = gd.getNextNumber(); + if (isText) { + angle = gd.getNextNumber(); + justification = gd.getNextChoiceIndex(); + } + if (!isLine) { + if (isPoint) { + int index = gd.getNextChoiceIndex(); + ((PointRoi)roi).setPointType(index); + index = gd.getNextChoiceIndex(); + ((PointRoi)roi).setSize(index); + } else + fillc = gd.getNextString(); + } + boolean applyToOverlay = false; + boolean newOverlay = addToOverlay?gd.getNextBoolean():false; + if (overlayOptions) { + setPositions = gd.getNextBoolean(); + if (overlay!=null) { + applyToOverlay = gd.getNextBoolean(); + boolean labels = gd.getNextBoolean(); + boolean hideOverlay = gd.getNextBoolean(); + if (hideOverlay && imp!=null) { + if (!imp.getHideOverlay()) + imp.setHideOverlay(true); + } else { + overlay.drawLabels(labels); + Analyzer.drawLabels(labels); + overlay.drawBackgrounds(true); + if (imp.getHideOverlay()) + imp.setHideOverlay(false); + if (!applyToOverlay && imp!=null) + imp.draw(); + } + } + roi.setPosition(setPositions?1:0); + } + if (isText) + antialias = gd.getNextBoolean(); + if (showListCoordinates) { + listCoordinates = gd.getNextBoolean(); + if (nProperties>0) + listProperties = gd.getNextBoolean(); + } + strokeColor = Colors.decode(linec, null); + fillColor = Colors.decode(fillc, null); + if (isText) { + TextRoi troi = (TextRoi)roi; + Font font = troi.getCurrentFont(); + if (strokeWidth2!=strokeWidth) { + font = new Font(font.getName(), font.getStyle(), (int)strokeWidth2); + troi.setCurrentFont(font); + } + troi.setAngle(angle); + if (justification!=troi.getJustification()) + troi.setJustification(justification); + troi.setAntiAlias(antialias); + if (!isRange) troi.setText(gd.getNextText()); + } else if (strokeWidth2!=strokeWidth) + roi.setStrokeWidth((float)strokeWidth2); + roi.setStrokeColor(strokeColor); + roi.setFillColor(fillColor); + if (showName) { + setPosition(roi, position, position2); + setGroup(roi, group, group2); + } + if (newOverlay) roi.setName("new-overlay"); + if (applyToOverlay) { + if (imp==null || overlay==null) + return true; + Roi[] rois = overlay.toArray(); + for (int i=0; i0) + listProperties(roi); + return true; + } + + private void setPosition(Roi roi, String pos1, String pos2) { + if (pos1.equals(pos2)) + return; + if (pos2.equals("none") || pos2.equals("0")) { + roi.setPosition(0); + return; + } + String[] positions = Tools.split(pos2, " ,"); + if (positions.length==1) { + double stackPos = Tools.parseDouble(positions[0]); + if (!Double.isNaN(stackPos)) + roi.setPosition((int)stackPos); + return; + } + if (positions.length==3) { + int[] pos = new int[3]; + for (int i=0; i<3; i++) { + double dpos = Tools.parseDouble(positions[i]); + if (Double.isNaN(dpos)) + return; + else + pos[i] = (int)dpos; + } + roi.setPosition(pos[0], pos[1], pos[2]); + return; + } + } + + private void setGroup(Roi roi, String group1, String group2) { + if (group1.equals(group2)) + return; + if (group2.equals("none") || group2.equals("0")) { + roi.setGroup(0); + return; + } + double group = Tools.parseDouble(group2); + if (!Double.isNaN(group)) + roi.setGroup((int)group); + } + + public boolean showImageDialog(String name) { + ImageRoi iRoi = (ImageRoi)roi; + boolean zeroTransparent = iRoi.getZeroTransparent(); + GenericDialog gd = new GenericDialog("Image ROI Properties"); + gd.addStringField("Name:", name, 15); + gd.addNumericField("Opacity (0-100%):", iRoi.getOpacity()*100.0, 0); + gd.addCheckbox("Transparent background", zeroTransparent); + if (addToOverlay) + gd.addCheckbox("New Overlay", false); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + name = gd.getNextString(); + roi.setName(name.length()>0?name:null); + double opacity = gd.getNextNumber()/100.0; + iRoi.setOpacity(opacity); + boolean zeroTransparent2 = gd.getNextBoolean(); + if (zeroTransparent!=zeroTransparent2) + iRoi.setZeroTransparent(zeroTransparent2); + boolean newOverlay = addToOverlay?gd.getNextBoolean():false; + if (newOverlay) roi.setName("new-overlay"); + return true; + } + + void listCoordinates(Roi roi) { + if (roi==null) return; + boolean allIntegers = true; + FloatPolygon fp = roi.getFloatPolygon(); + ImagePlus imp = roi.getImage(); + String title = "Coordinates"; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + int height = imp.getHeight(); + for (int i=0; i=0 && group<=255) { + roi.setGroup((int)group); + String name = Roi.getGroupName((int)group); + if (name==null) + name="unnamed"; + if (group==0) + name = ""; + groupName.setText(" "+name); + Color strokeColor = roi.getStrokeColor(); + colorField.setText(Colors.colorToString(strokeColor)); + } else + groupName.setText(""); + } + + public void windowActivated(WindowEvent e) { + if (groupName!=null) { + String gname = Roi.getGroupName(roi.getGroup()); + groupName.setText(gname!=null?" "+gname:""); // add space to separate label from field + } + } + + public void windowClosing(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + +} diff --git a/src/ij/gui/RotatedRectRoi.java b/src/ij/gui/RotatedRectRoi.java new file mode 100644 index 0000000..f2d7ff9 --- /dev/null +++ b/src/ij/gui/RotatedRectRoi.java @@ -0,0 +1,229 @@ +package ij.gui; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; +import ij.*; +import ij.plugin.frame.Recorder; +import ij.process.FloatPolygon; +import ij.measure.Calibration; + +/** This class implements the rotated rectangle selection tool. */ +public class RotatedRectRoi extends PolygonRoi { + private double xstart, ystart; + private static double DefaultRectWidth = 50; + private double rectWidth = DefaultRectWidth; + + public RotatedRectRoi(double x1, double y1, double x2, double y2, double rectWidth) { + super(new float[5], new float[5], 5, FREEROI); + this.rectWidth = rectWidth; + makeRectangle(x1, y1, x2, y2); + state = NORMAL; + bounds = null; + } + + public RotatedRectRoi(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + type = FREEROI; + xstart = offScreenXD(sx); + ystart = offScreenYD(sy); + ImageWindow win = imp.getWindow(); + int pixels = win!=null?(int)(win.getSize().height/win.getCanvas().getMagnification()):imp.getHeight(); + if (IJ.debugMode) IJ.log("RotatedRectRoi: "+(int)rectWidth+" "+pixels); + if (rectWidth>pixels) + rectWidth = pixels/3; + setDrawOffset(false); + bounds = null; + } + + public void draw(Graphics g) { + super.draw(g); + if (!overlay && ic!=null) { + double mag = ic.getMagnification(); + for (int i=0; i<4; i++) { + if (i==3) //mark starting point + handleColor = strokeColor!=null?strokeColor:ROIColor; + else + handleColor=Color.white; + drawHandle(g, hxs(i), hys(i)); + } + } + } + + private int hxs(int index) { + int indexPlus1 = index<3?index+1:0; + return xp2[index]+(xp2[indexPlus1]-xp2[index])/2; + } + + private int hys(int index) { + int indexPlus1 = index<3?index+1:0; + return yp2[index]+(yp2[indexPlus1]-yp2[index])/2; + } + + private double hx(int index) { + int indexPlus1 = index<3?index+1:0; + return xpf[index]+(xpf[indexPlus1]-xpf[index])/2+x; + } + + private double hy(int index) { + int indexPlus1 = index<3?index+1:0; + return ypf[index]+(ypf[indexPlus1]-ypf[index])/2+y; + } + + protected void grow(int sx, int sy) { + double x1 = xstart; + double y1 = ystart; + double x2 = offScreenXD(sx); + double y2 = offScreenYD(sy); + makeRectangle(x1, y1, x2, y2); + imp.draw(); + } + + void makeRectangle(double x1, double y1, double x2, double y2) { + double length = Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)); + double angle = Math.atan ((x2-x1)/(y2-y1)); + double wsa = (rectWidth/2.0)*Math.sin((Math.PI/2.0)+angle); + double wca = (rectWidth/2.0)*Math.cos((Math.PI/2)+angle); + nPoints = 5; + xpf[3] = (float)(x1-wsa); + ypf[3] = (float)(y1-wca); + xpf[0] = (float)(x1+wsa); + ypf[0] = (float)(y1+wca); + xpf[1] = (float)(x2+wsa); + ypf[1] = (float)(y2+wca); + xpf[2] = (float)(x2-wsa); + ypf[2] = (float)(y2-wca); + xpf[4] = xpf[0]; + ypf[4] = ypf[0]; + makePolygonRelative(); + cachedMask = null; + DefaultRectWidth = rectWidth; + showStatus(); + } + + public void showStatus() { + double[] p = getParams(); + double dx = p[2] - p[0]; + double dy = p[3] - p[1]; + double length = Math.sqrt(dx*dx+dy*dy); + double width = p[4]; + if (imp!=null && !IJ.altKeyDown()) { + Calibration cal = imp.getCalibration(); + if (cal.scaled() && cal.pixelWidth==cal.pixelHeight) { + dx *= cal.pixelWidth; + dy *= cal.pixelHeight; + length = Math.sqrt(dx*dx+dy*dy); + width = p[4]*cal.pixelWidth; + } + } + double angle = getFloatAngle(p[0], p[1], p[2], p[3]); + IJ.showStatus("length=" + IJ.d2s(length)+", width=" + IJ.d2s(width)+", angle=" + IJ.d2s(angle)); + } + + public void nudgeCorner(int key) { + if (ic==null) return; + double[] p = getParams(); + double x1 = p[0]; + double y1 = p[1]; + double x2 = p[2]; + double y2 = p[3]; + double inc = 1.0/ic.getMagnification(); + switch(key) { + case KeyEvent.VK_UP: y2-=inc; break; + case KeyEvent.VK_DOWN: y2+=inc; break; + case KeyEvent.VK_LEFT: x2-=inc; break; + case KeyEvent.VK_RIGHT: x2+=inc; break; + } + makeRectangle(x1, y1, x2, y2); + imp.draw(); + notifyListeners(RoiListener.MOVED); + showStatus(); + } + + void makePolygonRelative() { + FloatPolygon poly = new FloatPolygon(xpf, ypf, nPoints); + Rectangle r = poly.getBounds(); + x = r.x; + y = r.y; + width = r.width; + height = r.height; + bounds = null; + for (int i=0; i=sx2 && sx<=sx2+size && sy>=sy2 && sy<=sy2+size) { + index = i; + break; + } + } + return index; + } + + /** Returns x1, y1, x2, y2 and width as a 5 element array. */ + public double[] getParams() { + double[] params = new double[5]; + params[0] = hx(3); + params[1] = hy(3); + params[2] = hx(1); + params[3] = hy(1); + params[4] = rectWidth; + return params; + } + + /** Always returns true. */ + public boolean subPixelResolution() { + return true; + } + +} diff --git a/src/ij/gui/SaveChangesDialog.java b/src/ij/gui/SaveChangesDialog.java new file mode 100644 index 0000000..203fd11 --- /dev/null +++ b/src/ij/gui/SaveChangesDialog.java @@ -0,0 +1,98 @@ +package ij.gui; +import ij.IJ; +import java.awt.*; +import java.awt.event.*; + +/** A modal dialog box with a one line message and + "Don't Save", "Cancel" and "Save" buttons. */ +public class SaveChangesDialog extends Dialog implements ActionListener, KeyListener { + private Button dontSave, cancel, save; + private boolean cancelPressed, savePressed; + + public SaveChangesDialog(Frame parent, String fileName) { + super(parent, "Save?", true); + setLayout(new BorderLayout()); + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 10, 10)); + Component message; + if (fileName.startsWith("Save ")) + message = new Label(fileName); + else { + if (fileName.length()>22) + message = new MultiLineLabel("Save changes to\n" + "\"" + fileName + "\"?"); + else + message = new Label("Save changes to \"" + fileName + "\"?"); + } + message.setFont(new Font("Dialog", Font.BOLD, 12)); + panel.add(message); + add("Center", panel); + + panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.CENTER, 8, 8)); + save = new Button(" Save "); + save.addActionListener(this); + save.addKeyListener(this); + cancel = new Button(" Cancel "); + cancel.addActionListener(this); + cancel.addKeyListener(this); + dontSave = new Button("Don't Save"); + dontSave.addActionListener(this); + dontSave.addKeyListener(this); + if (ij.IJ.isMacintosh()) { + panel.add(dontSave); + panel.add(cancel); + panel.add(save); + } else { + panel.add(save); + panel.add(dontSave); + panel.add(cancel); + } + add("South", panel); + if (ij.IJ.isMacintosh()) + setResizable(false); + pack(); + GUI.centerOnImageJScreen(this); + show(); + } + + public void actionPerformed(ActionEvent e) { + if (e.getSource()==cancel) + cancelPressed = true; + else if (e.getSource()==save) + savePressed = true; + closeDialog(); + } + + /** Returns true if the user dismissed dialog by pressing "Cancel". */ + public boolean cancelPressed() { + if (cancelPressed) + ij.Macro.abort(); + return cancelPressed; + } + + /** Returns true if the user dismissed dialog by pressing "Save". */ + public boolean savePressed() { + return savePressed; + } + + void closeDialog() { + //setVisible(false); + dispose(); + } + + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyDown(keyCode); + if (keyCode==KeyEvent.VK_ENTER) + closeDialog(); + else if (keyCode==KeyEvent.VK_ESCAPE) { + cancelPressed = true; + closeDialog(); + IJ.resetEscape(); + } + } + + public void keyReleased(KeyEvent e) {} + public void keyTyped(KeyEvent e) {} + +} diff --git a/src/ij/gui/ScrollbarWithLabel.java b/src/ij/gui/ScrollbarWithLabel.java new file mode 100644 index 0000000..3d1d13a --- /dev/null +++ b/src/ij/gui/ScrollbarWithLabel.java @@ -0,0 +1,233 @@ +package ij.gui; +import ij.ImageJ; +import ij.IJ; +import ij.Prefs; +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.*; + + +/** This class, based on Joachim Walter's Image5D package, adds "c", "z" labels + and play-pause icons (T) to the stack and hyperstacks dimension sliders. + * @author Joachim Walter + */ +public class ScrollbarWithLabel extends Panel implements Adjustable, AdjustmentListener { + Scrollbar bar; + private Icon icon; + private StackWindow stackWindow; + transient AdjustmentListener adjustmentListener; + + public ScrollbarWithLabel() { + } + + public ScrollbarWithLabel(StackWindow stackWindow, int value, int visible, int minimum, int maximum, char label) { + super(new BorderLayout(2, 0)); + this.stackWindow = stackWindow; + bar = new Scrollbar(Scrollbar.HORIZONTAL, value, visible, minimum, maximum); + GUI.fixScrollbar(bar); + icon = new Icon(label); + add(icon, BorderLayout.WEST); + add(bar, BorderLayout.CENTER); + bar.addAdjustmentListener(this); + addKeyListener(IJ.getInstance()); + } + + /* (non-Javadoc) + * @see java.awt.Component#getPreferredSize() + */ + public Dimension getPreferredSize() { + Dimension dim = new Dimension(0,0); + int width = bar.getPreferredSize().width; + Dimension minSize = getMinimumSize(); + if (widthheight) + height = iconHeight; + dim = new Dimension(width, (int)(height)); + return dim; + } + + public Dimension getMinimumSize() { + return new Dimension(80, 15); + } + + /* Adds KeyListener also to all sub-components. + */ + public synchronized void addKeyListener(KeyListener l) { + super.addKeyListener(l); + bar.addKeyListener(l); + } + + /* Removes KeyListener also from all sub-components. + */ + public synchronized void removeKeyListener(KeyListener l) { + super.removeKeyListener(l); + bar.removeKeyListener(l); + } + + /* + * Methods of the Adjustable interface + */ + public synchronized void addAdjustmentListener(AdjustmentListener l) { + if (l == null) { + return; + } + adjustmentListener = AWTEventMulticaster.add(adjustmentListener, l); + } + public int getBlockIncrement() { + return bar.getBlockIncrement(); + } + public int getMaximum() { + return bar.getMaximum(); + } + public int getMinimum() { + return bar.getMinimum(); + } + public int getOrientation() { + return bar.getOrientation(); + } + public int getUnitIncrement() { + return bar.getUnitIncrement(); + } + public int getValue() { + return bar.getValue(); + } + public int getVisibleAmount() { + return bar.getVisibleAmount(); + } + public synchronized void removeAdjustmentListener(AdjustmentListener l) { + if (l == null) { + return; + } + adjustmentListener = AWTEventMulticaster.remove(adjustmentListener, l); + } + public void setBlockIncrement(int b) { + bar.setBlockIncrement(b); + } + public void setMaximum(int max) { + bar.setMaximum(max); + } + public void setMinimum(int min) { + bar.setMinimum(min); + } + public void setUnitIncrement(int u) { + bar.setUnitIncrement(u); + } + public void setValue(int v) { + bar.setValue(v); + } + public void setVisibleAmount(int v) { + bar.setVisibleAmount(v); + } + + public void setFocusable(boolean focusable) { + super.setFocusable(focusable); + bar.setFocusable(focusable); + } + + /* + * Method of the AdjustmenListener interface. + */ + public void adjustmentValueChanged(AdjustmentEvent e) { + if (bar != null && e.getSource() == bar) { + AdjustmentEvent myE = new AdjustmentEvent(this, e.getID(), e.getAdjustmentType(), + e.getValue(), e.getValueIsAdjusting()); + AdjustmentListener listener = adjustmentListener; + if (listener != null) { + listener.adjustmentValueChanged(myE); + } + } + } + + public void updatePlayPauseIcon() { + icon.repaint(); + } + + + class Icon extends Canvas implements MouseListener { + private final double SCALE = Prefs.getGuiScale(); + private final int WIDTH = (int)(12*SCALE); + private final int HEIGHT= (int)(14*SCALE); + private BasicStroke stroke = new BasicStroke((float)(2*SCALE)); + private char type; + private Image image; + + public Icon(char type) { + addMouseListener(this); + addKeyListener(IJ.getInstance()); + setSize(WIDTH, HEIGHT); + this.type = type; + } + + /** Overrides Component getPreferredSize(). */ + public Dimension getPreferredSize() { + return new Dimension(WIDTH, HEIGHT); + } + + public void update(Graphics g) { + paint(g); + } + + public void paint(Graphics g) { + g.setColor(Color.white); + g.fillRect(0, 0, WIDTH, HEIGHT); + Graphics2D g2d = (Graphics2D)g; + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + if (type=='t') + drawPlayPauseButton(g2d); + else + drawLetter(g); + } + + private void drawLetter(Graphics g) { + Font font = new Font("SansSerif", Font.PLAIN, 14); + if (SCALE>1.0) + font = font.deriveFont((float)(font.getSize()*SCALE)); + g.setFont(font); + g.setColor(Color.black); + g.drawString(String.valueOf(type), (int)(2*SCALE), (int)(12*SCALE)); + } + + private void drawPlayPauseButton(Graphics2D g) { + if (stackWindow.getAnimate()) { + g.setColor(Color.black); + g.setStroke(stroke); + int s3 = (int)(3*SCALE); + int s8 = (int)(8*SCALE); + int s11 = (int)(11*SCALE); + g.drawLine(s3, s3, s3, s11); + g.drawLine(s8, s3, s8, s11); + } else { + g.setColor(Color.darkGray); + GeneralPath path = new GeneralPath(); + path.moveTo(3f, 2f); + path.lineTo(10f, 7f); + path.lineTo(3f, 12f); + path.lineTo(3f, 2f); + if (SCALE>1.0) { + AffineTransform at = new AffineTransform(); + at.scale(SCALE, SCALE); + path = new GeneralPath(at.createTransformedShape(path)); + } + g.fill(path); + } + } + + public void mousePressed(MouseEvent e) { + if (type!='t') return; + int flags = e.getModifiers(); + if ((flags&(Event.ALT_MASK|Event.META_MASK|Event.CTRL_MASK))!=0) + IJ.doCommand("Animation Options..."); + else + IJ.doCommand("Start Animation [\\]"); + } + + public void mouseReleased(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + public void mouseClicked(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + + } // Icon class + +} diff --git a/src/ij/gui/ShapeRoi.java b/src/ij/gui/ShapeRoi.java new file mode 100644 index 0000000..1f943a5 --- /dev/null +++ b/src/ij/gui/ShapeRoi.java @@ -0,0 +1,1207 @@ +package ij.gui; +import java.awt.*; +import java.awt.image.*; +import java.awt.geom.*; +import java.awt.event.KeyEvent; +import java.util.*; +import ij.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.filter.Analyzer; +import ij.util.Tools; +import ij.util.FloatArray; + +/**A subclass of ij.gui.Roi (2D Regions Of Interest) implemented in terms of java.awt.Shape. + * A ShapeRoi is constructed from a ij.gui.Roi object, or as a result of logical operators + * (i.e., union, intersection, exclusive or, and subtraction) provided by this class. These operators use the package + * java.awt.geom as a backend.
+ * This code is in the public domain. + * @author Cezar M.Tigaret + */ +public class ShapeRoi extends Roi { + + /***/ + static final int NO_TYPE = 128; + + /**The maximum tolerance allowed in calculating the length of the curve segments of this ROI's shape.*/ + static final double MAXERROR = 1.0e-3; + + /** Coefficient used to obtain a flattened version of this ROI's shape. A flattened shape is the + * closest approximation of the original shape's curve segments with line segments. + * The FLATNESS is an indication of the maximum deviation between the flattened and the original shape. */ + static final double FLATNESS = 0.1; + + /** Flatness used for filling the shape when creating a mask. Lower values result in higher accuracy + * (determining which pixels near the border are filled), but lower speed when filling shapes with + * curved borders. */ + static final double FILL_FLATNESS = 0.01; + + /**Parsing a shape composed of linear segments less than this value will result in Roi objects of type + * {@link ij.gui.Roi#POLYLINE} and {@link ij.gui.Roi#POLYGON} for open and closed shapes, respectively. + * Conversion of shapes open and closed with more than MAXPOLY line segments will result, + * respectively, in {@link ij.gui.Roi#FREELINE} and {@link ij.gui.Roi#FREEROI} (or + * {@link ij.gui.Roi#TRACED_ROI} if {@link #forceTrace} flag is true. + */ + private static final int MAXPOLY = 10; // I hate arbitrary values !!!! + + private static final int OR=0, AND=1, XOR=2, NOT=3; + + /**The java.awt.Shape encapsulated by this object.*/ + private Shape shape; + + /**The instance value of the maximum tolerance (MAXERROR) allowed in calculating the + * length of the curve segments of this ROI's shape. + */ + private double maxerror = ShapeRoi.MAXERROR; + + /**The instance value of the coefficient (FLATNESS) used to + * obtain a flattened version of this ROI's shape. + */ + private double flatness = ShapeRoi.FLATNESS; + + /**The instance value of MAXPOLY.*/ + private int maxPoly = ShapeRoi.MAXPOLY; + + /**If true then methods that manipulate this ROI's shape will work on + * a flattened version of the shape. */ + private boolean flatten; + + /**Flag which specifies how Roi objects will be constructed from closed (sub)paths having more than + * MAXPOLY and composed exclusively of line segments. + * If true then (sub)path will be parsed into a + * {@link ij.gui.Roi#TRACED_ROI}; else, into a {@link ij.gui.Roi#FREEROI}. */ + private boolean forceTrace = false; + + /**Flag which specifies if Roi objects constructed from open (sub)paths composed of only two line segments + * will be of type {@link ij.gui.Roi#ANGLE}. + * If true then (sub)path will be parsed into a {@link ij.gui.Roi#ANGLE}; + * else, into a {@link ij.gui.Roi#POLYLINE}. */ + private boolean forceAngle = false; + + private Vector savedRois; //not really used any more + private static Stroke defaultStroke = new BasicStroke(); + + + /** Constructs a ShapeRoi from an Roi. */ + public ShapeRoi(Roi r) { + this(r, ShapeRoi.FLATNESS, ShapeRoi.MAXERROR, false, false, false, ShapeRoi.MAXPOLY); + } + + /** Constructs a ShapeRoi from a Shape. */ + public ShapeRoi(Shape s) { + super(s.getBounds()); + AffineTransform at = new AffineTransform(); + at.translate(-x, -y); + shape = new GeneralPath(at.createTransformedShape(s)); + type = COMPOSITE; + } + + /** Constructs a ShapeRoi from a Shape. */ + public ShapeRoi(int x, int y, Shape s) { + super(x, y, s.getBounds().width, s.getBounds().height); + shape = new GeneralPath(s); + type = COMPOSITE; + } + + /**Creates a ShapeRoi object from a "classical" ImageJ ROI. + * @param r An ij.gui.Roi object + * @param flatness The flatness factor used in convertion of curve segments into line segments. + * @param maxerror Error correction for calculating length of Bezeir curves. + * @param forceAngle flag used in the conversion of Shape objects to Roi objects (see {@link #shapeToRois()}. + * @param forceTrace flag for conversion of Shape objects to Roi objects (see {@link #shapeToRois()}. + * @param flatten if true then the shape of this ROI will be flattened + * (i.e., curve segments will be aproximated by line segments). + * @param maxPoly Roi objects constructed from shapes composed of linear segments fewer than this + * value will be of type {@link ij.gui.Roi#POLYLINE} or {@link ij.gui.Roi#POLYGON}; conversion of + * shapes with linear segments more than this value will result in Roi objects of type + * {@link ij.gui.Roi#FREELINE} or {@link ij.gui.Roi#FREEROI} unless the average side length + * is large (see {@link #shapeToRois()}). + */ + ShapeRoi(Roi r, double flatness, double maxerror, boolean forceAngle, boolean forceTrace, boolean flatten, int maxPoly) { + super(r.startX, r.startY, r.width, r.height); + this.type = COMPOSITE; + this.flatness = flatness; + this.maxerror = maxerror; + this.forceAngle = forceAngle; + this.forceTrace = forceTrace; + this.maxPoly= maxPoly; + this.flatten = flatten; + shape = roiToShape((Roi)r.clone()); + } + + /** Constructs a ShapeRoi from an array of variable length path segments. Each + segment consists of the segment type followed by 0-6 coordintes (0-3 end points and control + points). Depending on the type, a segment uses from 1 to 7 elements of the array. */ + public ShapeRoi(float[] shapeArray) { + super(0,0,null); + shape = makeShapeFromArray(shapeArray); + Rectangle r = shape.getBounds(); + x = r.x; + y = r.y; + width = r.width; + height = r.height; + state = NORMAL; + oldX=x; oldY=y; oldWidth=width; oldHeight=height; + AffineTransform at = new AffineTransform(); + at.translate(-x, -y); + shape = new GeneralPath(at.createTransformedShape(shape)); + flatness = ShapeRoi.FLATNESS; + maxerror = ShapeRoi.MAXERROR; + maxPoly = ShapeRoi.MAXPOLY; + flatten = false; + type = COMPOSITE; + } + + /**Returns a deep copy of this. */ + public synchronized Object clone() { // the equivalent of "operator=" ? + ShapeRoi sr = (ShapeRoi)super.clone(); + sr.type = COMPOSITE; + sr.flatness = flatness; + sr.maxerror = maxerror; + sr.forceAngle = forceAngle; + sr.forceTrace = forceTrace; + //sr.setImage(imp); //wsr + sr.setShape(ShapeRoi.cloneShape(shape)); + return sr; + } + + /** Returns a deep copy of the argument. */ + static Shape cloneShape(Shape rhs) { + if (rhs==null) return null; + else if (rhs instanceof Rectangle2D.Double) + return (Rectangle2D.Double)((Rectangle2D.Double)rhs).clone(); + else if (rhs instanceof Ellipse2D.Double) + return (Ellipse2D.Double)((Ellipse2D.Double)rhs).clone(); + else if (rhs instanceof Line2D.Double) + return (Line2D.Double)((Line2D.Double)rhs).clone(); + else if (rhs instanceof Polygon) + return new Polygon(((Polygon)rhs).xpoints, ((Polygon)rhs).ypoints, ((Polygon)rhs).npoints); + else if (rhs instanceof GeneralPath) + return (GeneralPath)((GeneralPath)rhs).clone(); + else + return makeShapeFromArray(getShapeAsArray(rhs, 0, 0)); + } + + /**********************************************************************************/ + /*** Logical operations on shaped rois ****/ + /**********************************************************************************/ + + /**Unary union operator. + * The caller is set to its union with the argument. + * @return the union of this and sr + */ + public ShapeRoi or(ShapeRoi sr) {return unaryOp(sr, OR);} + + /**Unary intersection operator. + * The caller is set to its intersection with the argument (i.e., the overlapping regions between the + * operands). + * @return the overlapping regions between this and sr + */ + public ShapeRoi and(ShapeRoi sr) {return unaryOp(sr, AND);} + + /**Unary exclusive or operator. + * The caller is set to the non-overlapping regions between the operands. + * @return the union of the non-overlapping regions of this and sr + * @see ij.gui.Roi#xor(Roi[]) + * @see ij.gui.Overlay#xor(int[]) + */ + public ShapeRoi xor(ShapeRoi sr) {return unaryOp(sr, XOR);} + + /**Unary subtraction operator. + * The caller is set to the result of the operation between the operands. + * @return this subtracted from sr + */ + public ShapeRoi not(ShapeRoi sr) {return unaryOp(sr, NOT);} + + ShapeRoi unaryOp(ShapeRoi sr, int op) { + AffineTransform at = new AffineTransform(); + at.translate(x, y); + Area a1 = new Area(at.createTransformedShape(getShape())); + at = new AffineTransform(); + at.translate(sr.x, sr.y); + Area a2 = new Area(at.createTransformedShape(sr.getShape())); + try { + switch (op) { + case OR: a1.add(a2); break; + case AND: a1.intersect(a2); break; + case XOR: a1.exclusiveOr(a2); break; + case NOT: a1.subtract(a2); break; + } + } catch(Exception e) {} + Rectangle r = a1.getBounds(); + at = new AffineTransform(); + at.translate(-r.x, -r.y); + setShape(new GeneralPath(at.createTransformedShape(a1))); + x = r.x; + y = r.y; + cachedMask = null; + return this; + } + + /**********************************************************************************/ + /*** Interconversions between "regular" rois and shaped rois ****/ + /**********************************************************************************/ + + /**Converts the Roi argument to an instance of java.awt.Shape. + * Currently, the following conversions are supported:
+ + + < + + + + + + + + + + + + + + +
Roi class Roi type Shape
ij.gui.Roi Roi.RECTANGLE java.awt.geom.Rectangle2D.Double
ij.gui.OvalRoi Roi.OVAL java.awt.Polygon of the corresponding traced roi
ij.gui.Line Roi.LINE java.awt.geom.Line2D.Double
ij.gui.PolygonRoi Roi.POLYGON java.awt.Polygon or (if subpixel resolution) closed java.awt.geom.GeneralPath
ij.gui.PolygonRoi Roi.FREEROI java.awt.Polygon or (if subpixel resolution) closed java.awt.geom.GeneralPath
ij.gui.PolygonRoi Roi.TRACED_ROI java.awt.Polygon or (if subpixel resolution) closed java.awt.geom.GeneralPath
ij.gui.PolygonRoi Roi.POLYLINE open java.awt.geom.GeneralPath
ij.gui.PolygonRoi Roi.FREELINE open java.awt.geom.GeneralPath
ij.gui.PolygonRoi Roi.ANGLE open java.awt.geom.GeneralPath
ij.gui.ShapeRoi Roi.COMPOSITE shape of argument
ij.gui.ShapeRoi ShapeRoi.NO_TYPE null
+ * + * @return A java.awt.geom.* object that inherits from java.awt.Shape interface. + * + */ + private Shape roiToShape(Roi roi) { + if (roi.isLine()) + roi = Roi.convertLineToArea(roi); + Shape shape = null; + Rectangle r = roi.getBounds(); + boolean closeShape = true; + int roiType = roi.getType(); + switch(roiType) { + case Roi.LINE: + Line line = (Line)roi; + shape = new Line2D.Double ((double)(line.x1-r.x), (double)(line.y1-r.y), (double)(line.x2-r.x), (double)(line.y2-r.y) ); + break; + case Roi.RECTANGLE: + int arcSize = roi.getCornerDiameter(); + if (arcSize>0) + shape = new RoundRectangle2D.Double(0, 0, r.width, r.height, arcSize, arcSize); + else + shape = new Rectangle2D.Double(0.0, 0.0, (double)r.width, (double)r.height); + break; + case Roi.POLYLINE: case Roi.FREELINE: case Roi.ANGLE: + closeShape = false; + case Roi.POLYGON: case Roi.FREEROI: case Roi.TRACED_ROI: case Roi.OVAL: + if (roiType == Roi.OVAL) { + //shape = new Ellipse2D.Double(-0.001, -0.001, r.width+0.002, r.height+0.002); //inaccurate (though better with increased diameter) + shape = ((OvalRoi)roi).getPolygon(false); + } else if (closeShape && !roi.subPixelResolution()) { + int nPoints =((PolygonRoi)roi).getNCoordinates(); + int[] xCoords = ((PolygonRoi)roi).getXCoordinates(); + int[] yCoords = ((PolygonRoi)roi).getYCoordinates(); + shape = new Polygon(xCoords, yCoords, nPoints); + } else { + FloatPolygon floatPoly = roi.getFloatPolygon(); + if (floatPoly.npoints <=1) break; + shape = new GeneralPath(closeShape ? GeneralPath.WIND_EVEN_ODD : GeneralPath.WIND_NON_ZERO, floatPoly.npoints); + ((GeneralPath)shape).moveTo(floatPoly.xpoints[0] - r.x, floatPoly.ypoints[0] - r.y); + for (int i=1; ior(ShapeRoi). */ + void saveRoi(Roi roi) { + if (savedRois==null) + savedRois = new Vector(); + savedRois.addElement(roi); + } + + /**Converts a Shape into Roi object(s). + *
This method parses the shape into (possibly more than one) Roi objects + * and returns them in an array. + *
A simple, "regular" path results in a single Roi following these simple rules: + + + + + + + + +
Shape type Roi class Roi type
java.awt.geom.Rectangle2D.Double ij.gui.Roi Roi.RECTANGLE
java.awt.geom.Ellipse2D.Double ij.gui.OvalRoi Roi.OVAL
java.awt.geom.Line2D.Double ij.gui.Line Roi.LINE
java.awt.Polygon ij.gui.PolygonRoi Roi.POLYGON
+ *

Each subpath of a java.awt.geom.GeneralPath is converted following these rules: + + + + + + + + + + + + + + + + + + + + + + + + +
Segment
types
Number of
segments
Closed
path
Value of
forceAngle
Value of
forceTrace
Roi type
lines only: 0 ShapeRoi.NO_TYPE
1 ShapeRoi.NO_TYPE
2 Y ShapeRoi.NO_TYPE
N Roi.LINE
3 Y N Roi.POLYGON
N Y Roi.ANGLE
N N Roi.POLYLINE
4 Y Roi.RECTANGLE
N Roi.POLYLINE
<= MAXPOLY Y Roi.POLYGON
N Roi.POLYLINE
> MAXPOLY Y Y Roi.TRACED_ROI
N Roi.FREEROI
N Roi.FREELINE
anything
else:
<= 2 ShapeRoi.NO_TYPE
> 2 ShapeRoi.SHAPE_ROI
+ * @return an array of ij.gui.Roi objects. + */ + public Roi[] getRois () { + if (shape==null) + return new Roi[0]; + if (savedRois!=null) + return (Roi[])savedRois.toArray(new Roi[savedRois.size()]); + ArrayList rois = new ArrayList(); + if (shape instanceof Rectangle2D.Double) { + Roi r = new Roi((int)((Rectangle2D.Double)shape).getX(), (int)((Rectangle2D.Double)shape).getY(), (int)((Rectangle2D.Double)shape).getWidth(), (int)((Rectangle2D.Double)shape).getHeight()); + rois.add(r); + } else if (shape instanceof Ellipse2D.Double) { + Roi r = new OvalRoi((int)((Ellipse2D.Double)shape).getX(), (int)((Ellipse2D.Double)shape).getY(), (int)((Ellipse2D.Double)shape).getWidth(), (int)((Ellipse2D.Double)shape).getHeight()); + rois.add(r); + } else if (shape instanceof Line2D.Double) { + Roi r = new ij.gui.Line((int)((Line2D.Double)shape).getX1(), (int)((Line2D.Double)shape).getY1(), (int)((Line2D.Double)shape).getX2(), (int)((Line2D.Double)shape).getY2()); + rois.add(r); + } else if (shape instanceof Polygon) { + Roi r = new PolygonRoi(((Polygon)shape).xpoints, ((Polygon)shape).ypoints, ((Polygon)shape).npoints, Roi.POLYGON); + rois.add(r); + } else { + PathIterator pIter; + if (flatten) + pIter = getFlatteningPathIterator(shape,flatness); + else + pIter = shape.getPathIterator(new AffineTransform()); + parsePath(pIter, ALL_ROIS, rois); + } + return (Roi[])rois.toArray(new Roi[rois.size()]); + } + + + /**Attempts to convert this ShapeRoi into a single non-composite Roi. + * @return an ij.gui.Roi object or null if it cannot be simplified to become a non-composite roi. + */ + public Roi shapeToRoi() { + if (shape==null || !(shape instanceof GeneralPath)) + return null; + PathIterator pIter = shape.getPathIterator(new AffineTransform()); + ArrayList rois = new ArrayList(); + parsePath(pIter, ONE_ROI, rois); + if (rois.size() == 1) + return (Roi)rois.get(0); + else + return null; + } + + /**Attempts to convert this ShapeRoi into a single non-composite Roi. + * For showing as a Roi, one should apply copyAttributes + * @return an ij.gui.Roi object, which is either the non-composite roi, + * or this ShapeRoi (if such a conversion is not possible) or null if + * this is an empty roi. + */ + public Roi trySimplify() { + Roi roi = shapeToRoi(); + return (roi==null) ? this : roi; + } + + /**Implements the rules of conversion from java.awt.geom.GeneralPath to ij.gui.Roi. + * @param nSegments The number of segments that compose the path (= number of vertices for a polygon) + * @param polygonLength length of polygon in pixels, or NaN if curved segments + * @param horizontalVerticalIntOnly Indicates whether the GeneralPath is composed of only vertical and horizontal lines with integer coordinates + * @param forceTrace Indicates that closed shapes with horizontalVerticalIntOnly=true should become TRACED_ROIs + * @param closed Indicates a closed GeneralPath + * @see #shapeToRois() + * @return a type flag like Roi.RECTANGLE or NO_TYPE if the type cannot be determined + */ + private int guessType(int nSegments, double polygonLength, boolean horizontalVerticalIntOnly, boolean forceTrace, boolean closed) { + int roiType = Roi.RECTANGLE; + if (Double.isNaN(polygonLength)) { + roiType = Roi.COMPOSITE; + } else { + // For more segments, they should be longer to qualify for a polygon with handles: + // The threshold for the average segment length is 4.0 for 4 segments, 16.0 for 64 segments, 32.0 for 256 segments + boolean longEdges = polygonLength/(nSegments*Math.sqrt(nSegments)) >= 2; + if (nSegments < 2) + roiType = NO_TYPE; + else if (nSegments == 2) + roiType = closed ? NO_TYPE : Roi.LINE; + else if (nSegments == 3 && !closed && forceAngle) + roiType = Roi.ANGLE; + else if (nSegments == 4 && closed && horizontalVerticalIntOnly && longEdges && !forceTrace && !this.forceTrace) + roiType = Roi.RECTANGLE; + else if (closed && horizontalVerticalIntOnly && (!longEdges || forceTrace || this.forceTrace)) + roiType = Roi.TRACED_ROI; + else if (nSegments <= MAXPOLY || longEdges) + roiType = closed ? Roi.POLYGON : Roi.POLYLINE; + else + roiType = closed ? Roi.FREEROI : Roi.FREELINE; + } + //IJ.log("guessType n= "+nSegments+" len="+polygonLength+" longE="+(polygonLength/(nSegments*Math.sqrt(nSegments)) >= 2)+" hvert="+horizontalVerticalIntOnly+" clos="+closed+" -> "+roiType); + return roiType; + } + + /**Creates a 'classical' (non-Shape) Roi object based on the arguments. + * @see #shapeToRois() + * @param xPoints the x coordinates + * @param yPoints the y coordinates + * @param type the type flag + * @return a ij.gui.Roi object or null + */ + private Roi createRoi(float[] xPoints, float[] yPoints, int roiType) { + if (roiType == NO_TYPE || roiType == Roi.COMPOSITE) return null; + Roi roi = null; + if (xPoints == null || yPoints == null || xPoints.length != yPoints.length || xPoints.length==0) return null; + + Tools.addToArray(xPoints, (float)getXBase()); + Tools.addToArray(yPoints, (float)getYBase()); + + switch(roiType) { + case Roi.LINE: roi = new ij.gui.Line(xPoints[0],yPoints[0],xPoints[1],yPoints[1]); break; + case Roi.RECTANGLE: + double[] xMinMax = Tools.getMinMax(xPoints); + double[] yMinMax = Tools.getMinMax(yPoints); + roi = new Roi((int)xMinMax[0], (int)yMinMax[0], + (int)xMinMax[1] - (int)xMinMax[0], (int)yMinMax[1] - (int)yMinMax[0]); + break; + case TRACED_ROI: + roi = new PolygonRoi(toIntR(xPoints), toIntR(yPoints), xPoints.length, roiType); + break; + default: + roi = new PolygonRoi(xPoints, yPoints, xPoints.length, roiType); + break; + } + return roi; + } + + /**********************************************************************************/ + /*** Geometry ****/ + /**********************************************************************************/ + + /** Checks whether the center of the specified pixel inside of this ROI's shape boundaries. + * Note the ImageJ convention of 0.5 pixel shift between outline and pixel center, + * i.e., pixel (0,0) is enclosed by the rectangle spanned between (0,0) and (1,1). + * The value slightly below 0.5 is for rounding according to the ImageJ convention + * (which is opposite to that of the java.awt.Shape class): + * In ImageJ, points exactly at the left (right) border are considered outside (inside); + * points exactly on horizontal borders, are considered outside (inside) at the border + * with the lower (higher) y. + */ + public boolean contains(int x, int y) { + if (shape==null) return false; + return shape.contains(x-this.x+0.494, y-this.y+0.49994); + } + + /** Returns whether coordinate (x,y) is contained in the Roi. + * Note that the coordinate (0,0) is the top-left corner of pixel (0,0). + * Use contains(int, int) to determine whether a given pixel is contained in the Roi. */ + public boolean containsPoint(double x, double y) { + if (!super.containsPoint(x, y)) + return false; + return shape.contains(x-this.x+1e-3, y-this.y+1e-6); //adding a bit to reduce the likelyhood of numerical errors at integers + } + + /** Returns the perimeter of this ShapeRoi. */ + public double getLength() { + if (width==0 && height==0) + return 0.0; + return parsePath(shape.getPathIterator(new AffineTransform()), GET_LENGTH, null); + } + + /** Returns a path iterator for this ROI's shape containing no curved (only straight) segments */ + PathIterator getFlatteningPathIterator(Shape s, double fl) { + return s.getPathIterator(new AffineTransform(),fl); + } + + /**Length of the control polygon of the cubic Bézier curve argument, in double precision.*/ + double cplength(CubicCurve2D.Double c) { + return Math.sqrt(sqr(c.ctrlx1-c.x1) + sqr(c.ctrly1-c.y1)) + + Math.sqrt(sqr(c.ctrlx2-c.ctrlx1) + sqr(c.ctrly2-c.ctrly1)) + + Math.sqrt(sqr(c.x2-c.ctrlx2) + sqr(c.y2-c.ctrly2)); + } + + /**Length of the control polygon of the quadratic Bézier curve argument, in double precision.*/ + double qplength(QuadCurve2D.Double c) { + return Math.sqrt(sqr(c.ctrlx-c.x1) + sqr(c.ctrly-c.y1)) + + Math.sqrt(sqr(c.x2-c.ctrlx) + sqr(c.y2-c.ctrly)); + } + + /**Length of the chord between the end points of the cubic Bézier curve argument, in double precision.*/ + double cclength(CubicCurve2D.Double c) { + return Math.sqrt(sqr(c.x2-c.x1) + sqr(c.y2-c.y1)); + } + + /**Length of the chord between the end points of the quadratic Bézier curve argument, in double precision.*/ + double qclength(QuadCurve2D.Double c) { + return Math.sqrt(sqr(c.x2-c.x1) + sqr(c.y2-c.y1)); + } + + /**Calculates the length of a cubic Bézier curve specified in double precision. + * The algorithm is based on the theory presented in paper
+ * "Jens Gravesen. Adaptive subdivision and the length and energy of Bézier curves. Computational Geometry 8:13-31 (1997)" + * implemented using java.awt.geom.CubicCurve2D.Double. + * Please visit {@link Graphics Gems IV} for + * examples of other possible implementations in C and C++. + */ + double cBezLength(CubicCurve2D.Double c) { + double l = 0.0; + double cl = cclength(c); + double pl = cplength(c); + if((pl-cl)/2.0 > maxerror) { + CubicCurve2D.Double[] cc = cBezSplit(c); + for(int i=0; i<2; i++) l+=cBezLength(cc[i]); + return l; + } + l = 0.5*pl+0.5*cl; + return l; + } + + /**Calculates the length of a quadratic Bézier curve specified in double precision. + * The algorithm is based on the theory presented in paper
+ * "Jens Gravesen. Adaptive subdivision and the length and energy of Bézier curves. Computational Geometry 8:13-31 (1997)" + * implemented using java.awt.geom.CubicCurve2D.Double. + * Please visit {@link Graphics Gems IV} for + * examples of other possible implementations in C and C++. + */ + double qBezLength(QuadCurve2D.Double c) { + double l = 0.0; + double cl = qclength(c); + double pl = qplength(c); + if((pl-cl)/2.0 > maxerror) + { + QuadCurve2D.Double[] cc = qBezSplit(c); + for(int i=0; i<2; i++) l+=qBezLength(cc[i]); + return l; + } + l = (2.0*pl+cl)/3.0; + return l; + } + + /**Splits a cubic Bézier curve in half. + * @param c A cubic Bézier curve to be divided + * @return an array with the left and right cubic Bézier subcurves + * + */ + CubicCurve2D.Double[] cBezSplit(CubicCurve2D.Double c) { + CubicCurve2D.Double[] cc = new CubicCurve2D.Double[2]; + for (int i=0; i<2 ; i++) cc[i] = new CubicCurve2D.Double(); + c.subdivide(cc[0],cc[1]); + return cc; + } + + /**Splits a quadratic Bézier curve in half. + * @param c A quadratic Bézier curve to be divided + * @return an array with the left and right quadratic Bézier subcurves + * + */ + QuadCurve2D.Double[] qBezSplit(QuadCurve2D.Double c) { + QuadCurve2D.Double[] cc = new QuadCurve2D.Double[2]; + for(int i=0; i<2; i++) cc[i] = new QuadCurve2D.Double(); + c.subdivide(cc[0],cc[1]); + return cc; + } + + // c is an array of even length with x0, y0, x1, y1, ... ,xn, yn coordinate pairs + /**Scales a coordinate array with the size calibration of a 2D image. + * The array is modified in place. + * @param c Array of coordinates in double precision with a fixed structure:
+ * x0,y0,x1,y1,....,xn,yn and with even length of 2*(n+1). + * @param pw The x-scale of the image. + * @param ph The y-scale of the image. + * @param n number of values in c that should be modified (must be less or equal to the size of c + * + */ + void scaleCoords(double[] c, int n, double pw, double ph) { + for(int i=0; i 0) { + addOffset(coords, nCoords, xBase, yBase); + shapeArray.add(coords, nCoords); + } + pIt.next(); + } + return shapeArray.toArray(); + } + + final static int ALL_ROIS=0, ONE_ROI=1, GET_LENGTH=2; //task types + final static int NO_SEGMENT_ANY_MORE = -1; //pseudo segment type when closed + /**Parses the geometry of this ROI's shape by means of the shape's PathIterator; + * Depending on the task and rois argument it will: + *
- create a single non-Shape Roi and add it to rois in case + * there is only one subpath, otherwise add this Roi unchanged to rois + * (task = ONE_ROI and rois non-null) + *
- add each subpath as a Roi to rois; curved subpaths will be flattened, i.e. converted to a + * polygon approximation (task != ONE_ROI and rois non-null) + *
- measure the combined length of all subpaths/Rois and return it (task = GET_LENGTH, rois may be null) + * @param pIter the PathIterator to be parsed. + * @param params an array with one element that will hold the calculated total length of the rois if its initial value is 0. + * If params holds the value SHAPE_TO_ROI, it will be tried to convert this ShapeRoi to a non-composite Roi. If this + * is not possible and this ShapeRoi is not empty, a reference to this ShapeRoi will be returned. + * @param rois an ArrayList that will hold ij.gui.Roi objects constructed from subpaths of this path; + * may be null only when task = GET_LENGTH + * (see @link #shapeToRois()} for details; + * @return Total length if task = GET_LENGTH.*/ + double parsePath(PathIterator pIter, int task, ArrayList rois) { + if (pIter==null || pIter.isDone()) + return 0.0; + double pw = 1.0, ph = 1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } + float xbase = (float)getXBase(); + float ybase = (float)getYBase(); + + FloatArray xPoints = new FloatArray(); //vertex coordinates of current subpath + FloatArray yPoints = new FloatArray(); + FloatArray shapeArray = new FloatArray(); //values for creating a GeneralPath for the current subpath + boolean getLength = task == GET_LENGTH; + int nSubPaths = 0; // the number of subpaths + boolean horVertOnly = true; // subpath has only horizontal or vertical lines + boolean closed = false; + //boolean success = false; + float[] fcoords = new float[6]; // unscaled float coordinates of the path segment + double[] coords = new double[6]; // scaled (calibrated) coordinates of the path segment + double startCalX = 0.0; // start x of subpath (scaled) + double startCalY = 0.0; // start y of subpath (scaled) + double lastCalX = 0.0; // x of previous point in the subpath (scaled) + double lastCalY = 0.0; // y of previous point in the subpath (scaled) + double pathLength = 0.0; // calibrated pathLength/perimeter of current curve + double totalLength = 0.0; // sum of all calibrated path lengths/perimeters + double uncalLength = 0.0; // uncalibrated length of polygon, NaN in case of curves + boolean done = false; + while (true) { + int segType = done ? NO_SEGMENT_ANY_MORE : pIter.currentSegment(fcoords); //read segment (if there is one more) + int nCoords = 0; //will be number of coordinates supplied with the segment + if (!done) { + nCoords = nCoords(segType); + if (getLength) { //make scaled coodinates to calculate the length + pIter.currentSegment(coords); + scaleCoords(coords, nCoords, pw, ph); + } + pIter.next(); + done = pIter.isDone(); + } + + //IJ.log("segType="+segType+" nCoord="+nCoords+" done="+done+" nPoi="+nPoints+" len="+pathLength); + if (segType == NO_SEGMENT_ANY_MORE || (segType == PathIterator.SEG_MOVETO && xPoints.size()>0)) { + // subpath finished: analyze it & create roi if appropriate + closed = closed || (xPoints.size()>0 && xPoints.get(0) == xPoints.getLast() && yPoints.get(0) == yPoints.getLast()); + float[] xpf = xPoints.toArray(); + float[] ypf = yPoints.toArray(); + if (Double.isNaN(uncalLength) || !allInteger(xpf) || !allInteger(ypf)) + horVertOnly = false; //allow conversion to rectangle or traced roi only for integer coordinates + boolean forceTrace = getLength && (!done || nSubPaths>0); //when calculating the length for >1 subpath, assume traced rois if it can be such + int roiType = guessType(xPoints.size(), uncalLength, horVertOnly, forceTrace, closed); + Roi roi = null; + if (roiType == COMPOSITE && rois != null) { //for ShapeRois with curves, we have the length from the path already, make roi only if needed + Shape shape = makeShapeFromArray(shapeArray.toArray()); //the curved subpath (image pixel coordinates) + FloatPolygon fp = getFloatPolygon(shape, FLATNESS, /*separateSubpaths=*/ false, /*addPointForClose=*/ false, /*absoluteCoord=*/ false); + roi = new PolygonRoi(fp, FREEROI); + } else if (roiType != NO_TYPE) { //NO_TYPE looks like an empty roi; only return non-empty rois + roi = createRoi(xpf, ypf, roiType); + } + if (rois != null && roi != null) + rois.add(roi); + if (task == ONE_ROI) { + if (rois.size() > 1) { //we can't make a single roi from this; so we can only keep the roi as it is + rois.clear(); + rois.add(this); + return 0.0; + } + } + if (getLength && roi != null && !Double.isNaN(uncalLength)) { + roi.setImage(imp); //calibration + pathLength = roi.getLength();//we don't use the path length of the Shape; e.g. for traced rois ImageJ has a better algorithm + roi.setImage(null); + } + totalLength += pathLength; + } + if (segType == NO_SEGMENT_ANY_MORE) // b r e a k t h e l o o p + return getLength ? totalLength : 0; + + closed = false; + switch(segType) { + case PathIterator.SEG_MOVETO: //we start a new subpath + xPoints.clear(); + yPoints.clear(); + shapeArray.clear(); + nSubPaths++; + pathLength = 0; + startCalX = coords[0]; + startCalY = coords[1]; + closed = false; + horVertOnly = true; + break; + case PathIterator.SEG_LINETO: + pathLength += Math.sqrt(sqr(lastCalY-coords[1])+sqr(lastCalX-coords[0])); + break; + case PathIterator.SEG_QUADTO: + if (getLength) { + QuadCurve2D.Double curve = new QuadCurve2D.Double(lastCalX,lastCalY,coords[0],coords[2],coords[2],coords[3]); + pathLength += qBezLength(curve); + } + uncalLength = Double.NaN; // not a polygon + break; + case PathIterator.SEG_CUBICTO: + if (getLength) { + CubicCurve2D.Double curve = new CubicCurve2D.Double(lastCalX,lastCalY,coords[0],coords[1],coords[2],coords[3],coords[4],coords[5]); + pathLength += cBezLength(curve); + } + uncalLength = Double.NaN; // not a polygon + break; + case PathIterator.SEG_CLOSE: + pathLength += Math.sqrt(sqr(lastCalX-startCalX) + sqr(lastCalY-startCalY)); + fcoords[0] = xPoints.get(0); //destination coordinates; with these we can handle it as SEG_LINETO + fcoords[1] = yPoints.get(0); + closed = true; + break; + default: + break; + } + if (xPoints.size()>0 && (segType == PathIterator.SEG_LINETO || segType == PathIterator.SEG_CLOSE)) { + float dx = fcoords[0] - xPoints.getLast(); + float dy = fcoords[1] - yPoints.getLast(); + uncalLength += Math.sqrt(sqr(dx) + sqr(dy)); + if (dx != 0f && dy != 0f) horVertOnly = false; + } + + if (nCoords > 0) { + xPoints.add(fcoords[nCoords - 2]); // the last coordinates are the end point of the segment + yPoints.add(fcoords[nCoords - 1]); + lastCalX = coords[nCoords - 2]; + lastCalY = coords[nCoords - 1]; + } + shapeArray.add(segType); + addOffset(fcoords, nCoords, xbase, ybase); // shift the shape to image origin + shapeArray.add(fcoords, nCoords(segType)); + } + } + + /** Non-destructively draws the shape of this object on the associated ImagePlus. */ + public void draw(Graphics g) { + Color color = strokeColor!=null? strokeColor:ROIColor; + boolean isActiveOverlayRoi = !overlay && isActiveOverlayRoi(); + //IJ.log("draw: "+overlay+" "+isActiveOverlayRoi); + if (isActiveOverlayRoi) { + if (color==Color.cyan) + color = Color.magenta; + else + color = Color.cyan; + } + if (fillColor!=null) color = fillColor; + g.setColor(color); + AffineTransform aTx = (((Graphics2D)g).getDeviceConfiguration()).getDefaultTransform(); + Graphics2D g2d = (Graphics2D)g; + if (stroke!=null && !isActiveOverlayRoi) + g2d.setStroke((ic!=null&&ic.getCustomRoi())||isCursor()?stroke:getScaledStroke()); + mag = getMagnification(); + int basex=0, basey=0; + if (ic!=null) { + Rectangle r = ic.getSrcRect(); + basex=r.x; basey=r.y; + } + aTx.setTransform(mag, 0.0, 0.0, mag, -basex*mag, -basey*mag); + aTx.translate(getXBase(), getYBase()); + if (fillColor!=null) { + if (isActiveOverlayRoi) { + g2d.setColor(Color.cyan); + g2d.draw(aTx.createTransformedShape(shape)); + } else + g2d.fill(aTx.createTransformedShape(shape)); + } else + g2d.draw(aTx.createTransformedShape(shape)); + if (stroke!=null) g2d.setStroke(defaultStroke); + if (Toolbar.getToolId()==Toolbar.OVAL) + drawRoiBrush(g); + if (state!=NORMAL && imp!=null && imp.getRoi()!=null) + showStatus(); + if (updateFullWindow) + {updateFullWindow = false; imp.draw();} + } + + public void drawRoiBrush(Graphics g) { + g.setColor(ROIColor); + int size = Toolbar.getBrushSize(); + if (size==0 || ic==null) + return; + int flags = ic.getModifiers(); + if ((flags&16)==0) return; // exit if mouse button up + int osize = size; + size = (int)(size*mag); + Point p = ic.getCursorLoc(); + int sx = ic.screenX(p.x); + int sy = ic.screenY(p.y); + int offset = (int)Math.round(ic.getMagnification()/2.0); + if ((osize&1)==0) + offset=0; // not needed when brush width even + g.drawOval(sx-size/2+offset, sy-size/2+offset, size, size); + } + + /**Draws the shape of this object onto the specified ImageProcessor. + *
This method will always draw a flattened version of the actual shape + * (i.e., all curve segments will be approximated by line segments). + */ + public void drawPixels(ImageProcessor ip) { + PathIterator pIter = shape.getPathIterator(new AffineTransform(), flatness); + float[] coords = new float[6]; + float sx=0f, sy=0f; + while (!pIter.isDone()) { + int segType = pIter.currentSegment(coords); + switch(segType) { + case PathIterator.SEG_MOVETO: + sx = coords[0]; + sy = coords[1]; + ip.moveTo(x+(int)sx, y+(int)sy); + break; + case PathIterator.SEG_LINETO: + ip.lineTo(x+(int)coords[0], y+(int)coords[1]); + break; + case PathIterator.SEG_CLOSE: + ip.lineTo(x+(int)sx, y+(int)sy); + break; + default: break; + } + pIter.next(); + } + } + + /** Returns this ROI's mask pixels as a ByteProcessor with pixels "in" the mask + * set to white (255) and pixels "outside" the mask set to black (0). + * Takes into account the usual ImageJ convention of 0.5 pxl shift between the outline and pixel + * coordinates; e.g., pixel (0,0) is surrounded by the rectangle spanned between (0,0) and (1,1). + * Note that apart from the 0.5 pixel shift, ImageJ has different convention for the border points + * than the java.awt.Shape class: + * In ImageJ, points exactly at the left (right) border are considered outside (inside); + * points exactly on horizontal borders, are considered outside (inside) at the border + * with the lower (higher) y. + * */ + public ImageProcessor getMask() { + if (shape==null) + return null; + ImageProcessor mask = cachedMask; + if (mask!=null && mask.getPixels()!=null && mask.getWidth()==width && mask.getHeight()==height) + return mask; + /* The following code using Graphics2D.fill would in principle work, but is very inaccurate + * at least with Oracle Java 8 or OpenJDK Java 10. + * For near-vertical polgon edges of 1000 pixels length, the deviation can be >0.8 pixels in x. + * Thus, approximating the shape by a polygon and using the PolygonFiller is more accurate + * (and roughly equally fast). --ms Jan 2018 */ + /*BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g2d = bi.createGraphics(); + g2d.setColor(Color.white); + g2d.transform(AffineTransform.getTranslateInstance(-0.48, -0.49994)); //very inaccurate, only reasonable with "-0.48" + g2d.fill(shape); + Raster raster = bi.getRaster(); + DataBufferByte buffer = (DataBufferByte)raster.getDataBuffer(); + byte[] mask = buffer.getData(); + cachedMask = new ByteProcessor(width, height, mask, null); + cachedMask.setThreshold(255,255,ImageProcessor.NO_LUT_UPDATE);*/ + FloatPolygon fpoly = getFloatPolygon(FILL_FLATNESS, true, false, false); + PolygonFiller pf = new PolygonFiller(fpoly.xpoints, fpoly.ypoints, fpoly.npoints, (float)(getXBase()-x), (float)(getYBase()-y)); + mask = pf.getMask(width, height); + cachedMask = mask; + return mask; + } + + /**Returns a reference to the Shape object encapsulated by this ShapeRoi. */ + public Shape getShape() { + return shape; + } + + /**Sets the java.awt.Shape object encapsulated by this + * to the argument. + *
This object will hold a (shallow) copy of the shape argument. If a deep copy + * of the shape argumnt is required, then a clone of the argument should be passed + * in; a possible example is setShape(ShapeRoi.cloneShape(shape)). + * @return false if the argument is null. + */ + boolean setShape(Shape rhs) { + boolean result = true; + if (rhs==null) return false; + if (shape.equals(rhs)) return false; + shape = rhs; + type = Roi.COMPOSITE; + Rectangle rect = shape.getBounds(); + width = rect.width; + height = rect.height; + return true; + } + + /**Returns the element with the smallest value in the array argument.*/ + private int min(int[] array) { + int val = array[0]; + for (int i=1; iaddPointForClose = false, there is no distinction between open and closed subpaths. + * @param absoluteCoord specifies whether the coordinates should be with respect to image bounds, not Roi bounds. */ + public FloatPolygon getFloatPolygon(double flatness, boolean separateSubpaths, boolean addPointForClose, boolean absoluteCoord) { + return getFloatPolygon(shape, flatness, separateSubpaths, addPointForClose, absoluteCoord); + } + + public FloatPolygon getFloatPolygon(Shape shape, double flatness, boolean separateSubpaths, boolean addPointForClose, boolean absoluteCoord) { + if (shape == null) return null; + PathIterator pIter = getFlatteningPathIterator(shape, flatness); + FloatArray xp = new FloatArray(); + FloatArray yp = new FloatArray(); + float[] coords = new float[6]; + int subPathStart = 0; + while (!pIter.isDone()) { + int segType = pIter.currentSegment(coords); + switch(segType) { + case PathIterator.SEG_MOVETO: + if (separateSubpaths && xp.size()>0 && !Float.isNaN(xp.get(xp.size()-1))) { + xp.add(Float.NaN); + yp.add(Float.NaN); + } + subPathStart = xp.size(); + case PathIterator.SEG_LINETO: + xp.add(coords[0]); + yp.add(coords[1]); + break; + case PathIterator.SEG_CLOSE: + boolean isClosed = xp.getLast() == xp.get(subPathStart) && yp.getLast() == yp.get(subPathStart); + if (addPointForClose && !isClosed) { + xp.add(xp.get(subPathStart)); + yp.add(yp.get(subPathStart)); + } else if (isClosed) { + xp.removeLast(1); //remove duplicate point if we should not add point to close the shape + yp.removeLast(1); + } + if (separateSubpaths && xp.size()>0 && !Float.isNaN(xp.get(xp.size()-1))) { + xp.add(Float.NaN); + yp.add(Float.NaN); + } + break; + default: + throw new RuntimeException("Invalid Segment Type: "+segType); + } + pIter.next(); + } + float[] xpf = xp.toArray(); + float[] ypf = yp.toArray(); + if (absoluteCoord) { + Tools.addToArray(xpf, (float)getXBase()); + Tools.addToArray(ypf, (float)getYBase()); + } + int n = xpf.length; + if (n>0 && Float.isNaN(xpf[n-1])) n--; //omit NaN at the end + return new FloatPolygon(xpf, ypf, n); + } + + public FloatPolygon getFloatConvexHull() { + FloatPolygon fp = getFloatPolygon(FLATNESS, /*separateSubpaths=*/ false, /*addPointForClose=*/ false, /*absoluteCoord=*/ true); + return fp == null ? null : fp.getConvexHull(); + } + + public Polygon getPolygon() { + FloatPolygon fp = getFloatPolygon(); + return new Polygon(toIntR(fp.xpoints), toIntR(fp.ypoints), fp.npoints); + } + + /** Returns all vertex points of the shape as approximated by polygons, + * in image pixel coordinates */ + public FloatPolygon getFloatPolygon() { + return getFloatPolygon(FLATNESS, /*separateSubpaths=*/ false, /*addPointForClose=*/ false, /*absoluteCoord=*/ true); + } + + /** Returns all vertex points of the shape as approximated by polygons, + * where options may include "close" to add points to close each subpath, and + * "separate" to insert NaN values between subpaths (= individual polygons) */ + public FloatPolygon getFloatPolygon(String options) { + options = options.toLowerCase(); + boolean separateSubpaths = options.indexOf("separate") >= 0; + boolean addPointForClose = options.indexOf("close") >= 0; + return getFloatPolygon(FLATNESS, separateSubpaths, addPointForClose, /*absoluteCoord=*/ true); + } + + /** Retuns the number of vertices, of this shape as approximated by straight lines. + * Note that points might be counted twice where the shape gets closed. */ + public int size() { + return getPolygon().npoints; + } + + boolean allInteger(float[] a) { + for (int i=0; i1 && previousSlice<=imp.getStackSize()) + imp.setSlice(previousSlice); + else + imp.setSlice(1); + thread = new Thread(this, "zSelector"); + thread.start(); + } + + void addScrollbars(ImagePlus imp) { + ImageStack s = imp.getStack(); + int stackSize = s.getSize(); + int sliderHeight = 0; + nSlices = stackSize; + hyperStack = imp.getOpenAsHyperStack(); + //imp.setOpenAsHyperStack(false); + int[] dim = imp.getDimensions(); + int nDimensions = 2+(dim[2]>1?1:0)+(dim[3]>1?1:0)+(dim[4]>1?1:0); + if (nDimensions<=3 && dim[2]!=nSlices) + hyperStack = false; + if (hyperStack) { + nChannels = dim[2]; + nSlices = dim[3]; + nFrames = dim[4]; + } + if (nSlices==stackSize) hyperStack = false; + if (nChannels*nSlices*nFrames!=stackSize) hyperStack = false; + if (cSelector!=null||zSelector!=null||tSelector!=null) + removeScrollbars(); + ImageJ ij = IJ.getInstance(); + //IJ.log("StackWindow: "+hyperStack+" "+nChannels+" "+nSlices+" "+nFrames+" "+imp); + if (nChannels>1) { + cSelector = new ScrollbarWithLabel(this, 1, 1, 1, nChannels+1, 'c'); + add(cSelector); + sliderHeight += cSelector.getPreferredSize().height + ImageWindow.VGAP; + if (ij!=null) cSelector.addKeyListener(ij); + cSelector.addAdjustmentListener(this); + cSelector.setFocusable(false); // prevents scroll bar from blinking on Windows + cSelector.setUnitIncrement(1); + cSelector.setBlockIncrement(1); + } + if (nSlices>1) { + char label = nChannels>1||nFrames>1?'z':'t'; + if (stackSize==dim[2] && imp.isComposite()) label = 'c'; + zSelector = new ScrollbarWithLabel(this, 1, 1, 1, nSlices+1, label); + if (label=='t') animationSelector = zSelector; + add(zSelector); + sliderHeight += zSelector.getPreferredSize().height + ImageWindow.VGAP; + if (ij!=null) zSelector.addKeyListener(ij); + zSelector.addAdjustmentListener(this); + zSelector.setFocusable(false); + int blockIncrement = nSlices/10; + if (blockIncrement<1) blockIncrement = 1; + zSelector.setUnitIncrement(1); + zSelector.setBlockIncrement(blockIncrement); + sliceSelector = zSelector.bar; + } + if (nFrames>1) { + animationSelector = tSelector = new ScrollbarWithLabel(this, 1, 1, 1, nFrames+1, 't'); + add(tSelector); + sliderHeight += tSelector.getPreferredSize().height + ImageWindow.VGAP; + if (ij!=null) tSelector.addKeyListener(ij); + tSelector.addAdjustmentListener(this); + tSelector.setFocusable(false); + int blockIncrement = nFrames/10; + if (blockIncrement<1) blockIncrement = 1; + tSelector.setUnitIncrement(1); + tSelector.setBlockIncrement(blockIncrement); + } + ImageWindow win = imp.getWindow(); + if (win!=null) + win.setSliderHeight(sliderHeight); + } + + /** Enables or disables the sliders. Used when locking/unlocking an image. */ + public synchronized void setSlidersEnabled(final boolean b) { + EventQueue.invokeLater(new Runnable() { + public void run() { + if (sliceSelector != null) sliceSelector.setEnabled(b); + if (cSelector != null) cSelector.setEnabled(b); + if (zSelector != null) zSelector.setEnabled(b); + if (tSelector != null) tSelector.setEnabled(b); + if (animationSelector != null) animationSelector.setEnabled(b); + } + }); + } + + public synchronized void adjustmentValueChanged(AdjustmentEvent e) { + if (!running2 || imp.isHyperStack()) { + if (e.getSource()==cSelector) { + c = cSelector.getValue(); + if (c==imp.getChannel()&&e.getAdjustmentType()==AdjustmentEvent.TRACK) return; + } else if (e.getSource()==zSelector) { + z = zSelector.getValue(); + int slice = hyperStack?imp.getSlice():imp.getCurrentSlice(); + if (z==slice&&e.getAdjustmentType()==AdjustmentEvent.TRACK) return; + } else if (e.getSource()==tSelector) { + t = tSelector.getValue(); + if (t==imp.getFrame()&&e.getAdjustmentType()==AdjustmentEvent.TRACK) return; + } + slice = (t-1)*nChannels*nSlices + (z-1)*nChannels + c; + notify(); + } + if (!running) + syncWindows(e.getSource()); + } + + private void syncWindows(Object source) { + if (SyncWindows.getInstance()==null) + return; + if (source==cSelector) + SyncWindows.setC(this, cSelector.getValue()); + else if (source==zSelector) { + int stackSize = imp.getStackSize(); + if (imp.getNChannels()==stackSize) + SyncWindows.setC(this, zSelector.getValue()); + else if (imp.getNFrames()==stackSize) + SyncWindows.setT(this, zSelector.getValue()); + else + SyncWindows.setZ(this, zSelector.getValue()); + } else if (source==tSelector) + SyncWindows.setT(this, tSelector.getValue()); + else + throw new RuntimeException("Unknownsource:"+source); + } + + public void actionPerformed(ActionEvent e) { + } + + public void mouseWheelMoved(MouseWheelEvent e) { + synchronized(this) { + int rotation = e.getWheelRotation(); + boolean ctrl = (e.getModifiers()&Event.CTRL_MASK)!=0; + if ((ctrl||IJ.shiftKeyDown()) && ic!=null) { + Point loc = ic.getCursorLoc(); + int x = ic.screenX(loc.x); + int y = ic.screenY(loc.y); + if (rotation<0) + ic.zoomIn(x,y); + else + ic.zoomOut(x,y); + return; + } + if (hyperStack) { + if (rotation>0) + IJ.run(imp, "Next Slice [>]", ""); + else if (rotation<0) + IJ.run(imp, "Previous Slice [<]", ""); + } else { + int slice = imp.getCurrentSlice() + rotation; + if (slice<1) + slice = 1; + else if (slice>imp.getStack().getSize()) + slice = imp.getStack().getSize(); + setSlice(imp,slice); + imp.updateStatusbarValue(); + SyncWindows.setZ(this, slice); + } + } + } + + public boolean close() { + if (!super.close()) + return false; + synchronized(this) { + done = true; + notify(); + } + return true; + } + + /** Displays the specified slice and updates the stack scrollbar. */ + public void showSlice(int index) { + if (imp!=null && index>=1 && index<=imp.getStackSize()) { + setSlice(imp,index); + SyncWindows.setZ(this, index); + } + } + + /** Updates the stack scrollbar. */ + public void updateSliceSelector() { + if (hyperStack || zSelector==null || imp==null) + return; + int stackSize = imp.getStackSize(); + int max = zSelector.getMaximum(); + if (max!=(stackSize+1)) + zSelector.setMaximum(stackSize+1); + EventQueue.invokeLater(new Runnable() { + public void run() { + if (imp!=null && zSelector!=null) + zSelector.setValue(imp.getCurrentSlice()); + } + }); + } + + public void run() { + while (!done) { + synchronized(this) { + try {wait();} + catch(InterruptedException e) {} + } + if (done) return; + if (slice>0) { + int s = slice; + slice = 0; + if (s!=imp.getCurrentSlice()) { + imp.updatePosition(c, z, t); + setSlice(imp,s); + } + } + } + } + + public String createSubtitle() { + String subtitle = super.createSubtitle(); + if (!hyperStack || imp.getStackSize()==1) + return subtitle; + String s=""; + int[] dim = imp.getDimensions(false); + int channels=dim[2], slices=dim[3], frames=dim[4]; + if (channels>1) { + s += "c:"+imp.getChannel()+"/"+channels; + if (slices>1||frames>1) s += " "; + } + if (slices>1) { + s += "z:"+imp.getSlice()+"/"+slices; + if (frames>1) s += " "; + } + if (frames>1) + s += "t:"+imp.getFrame()+"/"+frames; + if (running2) return s; + int index = subtitle.indexOf(";"); + if (index!=-1) { + int index2 = subtitle.indexOf("("); + if (index2>=0 && index2index2+4 && !subtitle.substring(index2+1, index2+4).equals("ch:")) { + index = index2; + s = s + " "; + } + subtitle = subtitle.substring(index, subtitle.length()); + } else + subtitle = ""; + return s + subtitle; + } + + public boolean isHyperStack() { + return hyperStack && getNScrollbars()>0; + } + + public void setPosition(int channel, int slice, int frame) { + if (cSelector!=null && channel!=c) { + c = channel; + cSelector.setValue(channel); + SyncWindows.setC(this, channel); + } + if (zSelector!=null && slice!=z) { + z = slice; + zSelector.setValue(slice); + SyncWindows.setZ(this, slice); + } + if (tSelector!=null && frame!=t) { + t = frame; + tSelector.setValue(frame); + SyncWindows.setT(this, frame); + } + this.slice = (t-1)*nChannels*nSlices + (z-1)*nChannels + c; + imp.updatePosition(c, z, t); + if (this.slice>0) { + int s = this.slice; + this.slice = 0; + if (s!=imp.getCurrentSlice()) + imp.setSlice(s); + } + } + + private void setSlice(ImagePlus imp, int n) { + if (imp.isLocked()) { + IJ.beep(); + IJ.showStatus("Image is locked"); + } else + imp.setSlice(n); + } + + public boolean validDimensions() { + int c = imp.getNChannels(); + int z = imp.getNSlices(); + int t = imp.getNFrames(); + //IJ.log(c+" "+z+" "+t+" "+nChannels+" "+nSlices+" "+nFrames+" "+imp.getStackSize()); + int size = imp.getStackSize(); + if (c==size && c*z*t==size && nSlices==size && nChannels*nSlices*nFrames==size) + return true; + if (c!=nChannels||z!=nSlices||t!=nFrames||c*z*t!=size) + return false; + else + return true; + } + + public void setAnimate(boolean b) { + if (running2!=b && animationSelector!=null) + animationSelector.updatePlayPauseIcon(); + running2 = b; + } + + public boolean getAnimate() { + return running2; + } + + public int getNScrollbars() { + int n = 0; + if (cSelector!=null) n++; + if (zSelector!=null) n++; + if (tSelector!=null) n++; + return n; + } + + void removeScrollbars() { + if (cSelector!=null) { + remove(cSelector); + cSelector.removeAdjustmentListener(this); + cSelector = null; + } + if (zSelector!=null) { + remove(zSelector); + zSelector.removeAdjustmentListener(this); + zSelector = null; + } + if (tSelector!=null) { + remove(tSelector); + tSelector.removeAdjustmentListener(this); + tSelector = null; + } + } + +} diff --git a/src/ij/gui/TextRoi.java b/src/ij/gui/TextRoi.java new file mode 100644 index 0000000..44b920a --- /dev/null +++ b/src/ij/gui/TextRoi.java @@ -0,0 +1,743 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.util.*; +import ij.macro.Interpreter; +import ij.plugin.frame.Recorder; +import ij.plugin.Colors; +import java.awt.geom.*; +import java.awt.*; +import java.awt.image.BufferedImage; + + +/** This class is a rectangular ROI containing text. */ +public class TextRoi extends Roi { + + public static final int LEFT=0, CENTER=1, RIGHT=2; + static final int MAX_LINES = 50; + + private static final String line1 = "Enter text, then press"; + private static final String line2 = "ctrl+b to add to overlay"; + private static final String line3 = "or ctrl+d to draw."; + private static final String line1a = "Enter text..."; + private String[] theText = new String[MAX_LINES]; + private static String name = "SansSerif"; + private static int style = Font.PLAIN; + private static int size = 18; + private Font font; + private static boolean antialiasedText = true; // global flag used by text tool + private static int globalJustification = LEFT; + private static Color defaultFillColor; + private int justification = LEFT; + private double previousMag; + private boolean firstChar = true; + private boolean firstMouseUp = true; + private double angle; // degrees + private static double defaultAngle; + private static boolean firstTime = true; + private Roi previousRoi; + private Graphics fontGraphics; + private static Font defaultFont = IJ.font12; + + /** Creates a TextRoi using the defaultFont.*/ + public TextRoi(int x, int y, String text) { + this(x, y, text, defaultFont); + } + + /** Use this constructor as a drop-in replacement for ImageProcessor.drawString(). */ + public TextRoi(String text, double x, double y, Font font) { + super(x, y, 1, 1); + init(text,font); + if (font!=null) { + Graphics g = getFontGraphics(font); + FontMetrics metrics = g.getFontMetrics(font); + Rectangle2D.Double fbounds = getFloatBounds(); + fbounds.y = fbounds.y-metrics.getAscent(); + setBounds(fbounds); + } + } + + /** Creates a TextRoi using sub-pixel coordinates.*/ + public TextRoi(double x, double y, String text) { + super(x, y, 1.0, 1.0); + init(text, null); + } + + /** Creates a TextRoi using the specified location and Font. + * @see ij.gui.Roi#setStrokeColor + * @see ij.gui.Roi#setNonScalable + * @see ij.ImagePlus#setOverlay(ij.gui.Overlay) + */ + public TextRoi(int x, int y, String text, Font font) { + super(x, y, 1, 1); + init(text, font); + } + + /** Creates a TextRoi using the specified sub-pixel location and Font. */ + public TextRoi(double x, double y, String text, Font font) { + super(x, y, 1.0, 1.0); + init(text, font); + } + + /** Creates a TextRoi using the specified sub-pixel location, size and Font. */ + public TextRoi(double x, double y, double width, double height, String text, Font font) { + super(x, y, width, height); + init(text, font); + } + + /** Creates a TextRoi using the specified text and location. */ + public static TextRoi create(String text, double x, double y, Font font) { + return new TextRoi(text, x, y, font); + } + + /** Obsolete. */ + public static TextRoi create(double x, double y, String text, Font font) { + return new TextRoi(x, y, text, font); + } + + private void init(String text, Font font) { + String[] lines = Tools.split(text, "\n"); + int count = Math.min(lines.length, MAX_LINES); + for (int i=0; i1.0) + mag = 1.0; + if (size<(12/mag)) + size = (int)(12/mag); + if (firstTime) { + theText[0] = line1; + theText[1] = line2; + theText[2] = line3; + firstTime = false; + } else + theText[0] = line1a; + if (previousRoi!=null && (previousRoi instanceof TextRoi)) { + firstMouseUp = false; + previousRoi = null; + } + font = new Font(name, style, size); + justification = globalJustification; + setStrokeColor(Toolbar.getForegroundColor()); + setAntiAlias(antialiasedText); + if (WindowManager.getWindow("Fonts")!=null) { + setFillColor(defaultFillColor); + setAngle(defaultAngle); + } + } + + /** This method is used by the text tool to add typed + characters to displayed text selections. */ + public void addChar(char c) { + if (imp==null) return; + if (!(c>=' ' || c=='\b' || c=='\n')) return; + int cline = 0; + if (firstChar) { + theText[cline] = new String(""); + for (int i=1; i0) + theText[cline] = theText[cline].substring(0, theText[cline].length()-1); + else if (cline>0) { + theText[cline] = null; + cline--; + } + if (angle!=0.0) + imp.draw(); + else + imp.draw(clipX, clipY, clipWidth, clipHeight); + firstChar = false; + return; + } else if ((int)c=='\n') { + // newline + if (cline<(MAX_LINES-1)) cline++; + theText[cline] = ""; + updateBounds(); + updateText(); + } else { + char[] chr = {c}; + theText[cline] += new String(chr); + updateBounds(); + updateText(); + firstChar = false; + return; + } + } + + Font getScaledFont() { + if (font==null) + font = new Font("SansSerif", Font.PLAIN, 14); + double mag = getMagnification(); + if (nonScalable || imp==null || mag==1.0) + return font; + else + return font.deriveFont((float)(font.getSize()*mag)); + } + + /** Renders the text on the image. Draws the text in + * the foreground color if ip.setColor(Color) has + * not been called. + * @see ij.process.ImageProcessor#setFont(Font) + * @see ij.process.ImageProcessor#setAntialiasedText(boolean) + * @see ij.process.ImageProcessor#setColor(Color) + */ + public void drawPixels(ImageProcessor ip) { + if (!ip.fillValueSet()) + ip.setColor(Toolbar.getForegroundColor()); + ip.setFont(font); + ip.setAntialiasedText(getAntiAlias()); + FontMetrics metrics = ip.getFontMetrics(); + int fontHeight = metrics.getHeight(); + int descent = metrics.getDescent(); + int i = 0; + int yy = 0; + int xi = (int)Math.round(getXBase()); + int yi = (int)Math.round(getYBase()); + while (i0.0) + setAntiAlias(true); + } + + /** Returns the state of the 'antiAlias' instance variable. */ + public boolean getAntialiased() { + return getAntiAlias(); + } + + /** Sets the default text tool justification (LEFT, CENTER or RIGHT). */ + public static void setGlobalJustification(int justification) { + if (justification<0 || justification>RIGHT) + justification = LEFT; + globalJustification = justification; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + Roi roi = imp.getRoi(); + if (roi instanceof TextRoi) { + ((TextRoi)roi).setJustification(justification); + imp.draw(); + } + } + } + + /** Returns the default text tool justification (LEFT, CENTER or RIGHT). */ + public static int getGlobalJustification() { + return globalJustification; + } + + /** Sets the 'justification' instance variable (LEFT, CENTER or RIGHT) */ + public void setJustification(int justification) { + if (justification<0 || justification>RIGHT) + justification = LEFT; + this.justification = justification; + updateBounds(); + if (imp!=null) + imp.draw(); + } + + /** Returns the value of the 'justification' instance variable (LEFT, CENTER or RIGHT). */ + public int getJustification() { + return justification; + } + + /** Sets the global font face, size and style that will be used by + TextROIs interactively created using the text tool. */ + public static void setFont(String fontName, int fontSize, int fontStyle) { + setFont(fontName, fontSize, fontStyle, true); + } + + /** Sets the font face, size, style and antialiasing mode that will + be used by TextROIs interactively created using the text tool. */ + public static void setFont(String fontName, int fontSize, int fontStyle, boolean antialiased) { + name = fontName; + size = fontSize; + style = fontStyle; + globalJustification = LEFT; + antialiasedText = antialiased; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + Roi roi = imp.getRoi(); + if (roi instanceof TextRoi) { + roi.setAntiAlias(antialiased); + ((TextRoi)roi).setCurrentFont(new Font(name, style, size)); + imp.draw(); + } + } + + } + + /** Sets the default font. */ + public static void setDefaultFont(Font font) { + defaultFont = font; + } + + /** Sets the default font size. */ + public static void setDefaultFontSize(int size) { + defaultFont = defaultFont.deriveFont((float)size); + } + + /** Sets the default fill (background) color. */ + public static void setDefaultFillColor(Color fillColor) { + defaultFillColor = fillColor; + } + + /** Sets the default angle. */ + public static void setDefaultAngle(double angle) { + defaultAngle = angle; + } + + protected void handleMouseUp(int screenX, int screenY) { + super.handleMouseUp(screenX, screenY); + if (this.width<5 && this.height<5 && imp!=null && previousRoi==null) { + int ox = ic!=null?ic.offScreenX(screenX):screenX; + int oy = ic!=null?ic.offScreenY(screenY):screenY; + TextRoi roi = new TextRoi(ox, oy, line1a); + roi.setStrokeColor(Toolbar.getForegroundColor()); + roi.firstChar = true; + imp.setRoi(roi); + return; + } else if (firstMouseUp) { + updateBounds(); + updateText(); + firstMouseUp = false; + } + if (this.width<5 || this.height<5) + imp.deleteRoi(); + } + + /** Increases the size of bounding rectangle so it's large enough to hold the text. */ + private void updateBounds() { + if (firstChar ) + return; + double lineHeight = 0; + double mag = getMagnification(); + Font font = getScaledFont(); + Graphics g = getFontGraphics(font); + Java2.setAntialiasedText(g, getAntiAlias()); + FontMetrics metrics = g.getFontMetrics(font); + double fontHeight = metrics.getHeight()/mag; + int i=0, nLines=0; + Rectangle2D.Double b = getFloatBounds(); + double newWidth = 10; + while (inewWidth) + newWidth = w; + i++; + } + newWidth += 2.0; + b.width = newWidth; + switch (justification) { + case LEFT: + break; + case CENTER: + b.x = this.oldX+this.oldWidth - newWidth/2.0; + break; + case RIGHT: + b.x = this.oldX+this.oldWidth - newWidth; + break; + } + b.height = nLines*fontHeight+2; + setBounds(b); + } + + private Graphics getFontGraphics(Font font) { + if (fontGraphics==null) { + BufferedImage bi =new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + fontGraphics = (Graphics2D)bi.getGraphics(); + } + fontGraphics.setFont(font); + return fontGraphics; + } + + void updateText() { + if (imp!=null) { + updateClipRect(); + if (angle!=0.0) + imp.draw(); + else + imp.draw(clipX, clipY, clipWidth, clipHeight); + } + } + + double stringWidth(String s, FontMetrics metrics, Graphics g) { + java.awt.geom.Rectangle2D r = metrics.getStringBounds(s, g); + return r.getWidth(); + } + + /** Used by the Recorder for recording the text tool. */ + public String getMacroCode(String cmd, ImagePlus imp) { + String code = ""; + boolean script = Recorder.scriptMode(); + boolean addSelection = cmd.startsWith("Add"); + if (script && !addSelection) + code += "ip = imp.getProcessor();\n"; + if (script) { + String str = "Font.PLAIN"; + if (style==Font.BOLD) + str = "Font.BOLD"; + else if (style==Font.ITALIC) + str = "Font.ITALIC"; + code += "font = new Font(\""+name+"\", "+str+", "+size+");\n"; + if (addSelection) + return getAddSelectionScript(code); + code += "ip.setFont(font);\n"; + } else { + String options = ""; + if (style==Font.BOLD) + options += "bold"; + if (style==Font.ITALIC) + options += " italic"; + if (antialiasedText) + options += " antialiased"; + if (options.equals("")) + options = "plain"; + code += "setFont(\""+name+"\", "+size+", \""+options+"\");\n"; + } + ImageProcessor ip = imp.getProcessor(); + ip.setFont(new Font(name, style, size)); + FontMetrics metrics = ip.getFontMetrics(); + int fontHeight = metrics.getHeight(); + if (script) + code += "ip.setColor(new Color("+getColorArgs(getStrokeColor())+"));\n"; + else + code += "setColor(\""+Colors.colorToString(getStrokeColor())+"\");\n"; + if (addSelection) { + code += "Overlay.drawString(\""+text()+"\", "+this.x+", "+(this.y+fontHeight)+", "+getAngle()+");\n"; + code += "Overlay.show();\n"; + } else { + code += (script?"ip.":"")+"drawString(\""+text()+"\", "+this.x+", "+(this.y+fontHeight)+");\n"; + if (script) + code += "imp.updateAndDraw();\n"; + else + code += "//makeText(\""+text()+"\", "+this.x+", "+(this.y+fontHeight)+");\n"; + } + return (code); + } + + private String text() { + String text = ""; + for (int i=0; iLEFT) { + if (just==CENTER) + code += "roi.setJustification(TextRoi.CENTER);\n"; + else if (just==RIGHT) + code += "roi.setJustification(TextRoi.RIGHT);\n"; + } + if (getAngle()!=0.0) + code += "roi.setAngle("+getAngle()+");\n"; + code += "overlay.add(roi);\n"; + return code; + } + + private String getColorArgs(Color c) { + return IJ.d2s(c.getRed()/255.0,2)+", "+IJ.d2s(c.getGreen()/255.0,2)+", "+IJ.d2s(c.getBlue()/255.0,2); + } + + public String getText() { + String text = ""; + for (int i=0; iw); + w = w2; + i++; + } + Rectangle r = ip.getRoi(); + if (w>r.width) { + r.width = w; + ip.setRoi(r); + } + ip.fill(); + } + } + + @Override + public void setLocation(int x, int y) { + super.setLocation(x, y); + oldWidth = this.width; + } + + /** Returns a copy of this TextRoi. */ + public synchronized Object clone() { + TextRoi tr = (TextRoi)super.clone(); + tr.theText = new String[MAX_LINES]; + for (int i=0; i>" + addPopupMenus(); + } + + public void init() { + dscale = Prefs.getGuiScale(); + scale = (int)Math.round(dscale); + if ((dscale>=1.5&&dscale<2.0) || (dscale>=2.5&&dscale<3.0)) + dscale = scale; + if (dscale>1.0) { + buttonWidth = (int)((BUTTON_WIDTH-2)*dscale); + buttonHeight = (int)((BUTTON_HEIGHT-2)*dscale); + offset = (int)Math.round((OFFSET-1)*dscale); + } else { + buttonWidth = BUTTON_WIDTH; + buttonHeight = BUTTON_HEIGHT; + offset = OFFSET; + } + gapSize = GAP_SIZE; + ps = new Dimension(buttonWidth*NUM_BUTTONS-(buttonWidth-gapSize), buttonHeight); + } + + void addPopupMenus() { + rectPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + rectPopup.setFont(Menus.getFont()); + rectItem = new CheckboxMenuItem("Rectangle", rectType==RECT_ROI); + rectItem.addItemListener(this); + rectPopup.add(rectItem); + roundRectItem = new CheckboxMenuItem("Rounded Rectangle", rectType==ROUNDED_RECT_ROI); + roundRectItem.addItemListener(this); + rectPopup.add(roundRectItem); + rotatedRectItem = new CheckboxMenuItem("Rotated Rectangle", rectType==ROTATED_RECT_ROI); + rotatedRectItem.addItemListener(this); + rectPopup.add(rotatedRectItem); + add(rectPopup); + + ovalPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + ovalPopup.setFont(Menus.getFont()); + ovalItem = new CheckboxMenuItem("Oval selections", ovalType==OVAL_ROI); + ovalItem.addItemListener(this); + ovalPopup.add(ovalItem); + ellipseItem = new CheckboxMenuItem("Elliptical selections", ovalType==ELLIPSE_ROI); + ellipseItem.addItemListener(this); + ovalPopup.add(ellipseItem); + brushItem = new CheckboxMenuItem("Selection Brush Tool", ovalType==BRUSH_ROI); + brushItem.addItemListener(this); + ovalPopup.add(brushItem); + add(ovalPopup); + + pointPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + pointPopup.setFont(Menus.getFont()); + pointItem = new CheckboxMenuItem("Point Tool", !multiPointMode); + pointItem.addItemListener(this); + pointPopup.add(pointItem); + multiPointItem = new CheckboxMenuItem("Multi-point Tool", multiPointMode); + multiPointItem.addItemListener(this); + pointPopup.add(multiPointItem); + add(pointPopup); + + linePopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + linePopup.setFont(Menus.getFont()); + straightLineItem = new CheckboxMenuItem("Straight Line", lineType==LINE&&!arrowMode); + straightLineItem.addItemListener(this); + linePopup.add(straightLineItem); + polyLineItem = new CheckboxMenuItem("Segmented Line", lineType==POLYLINE); + polyLineItem.addItemListener(this); + linePopup.add(polyLineItem); + freeLineItem = new CheckboxMenuItem("Freehand Line", lineType==FREELINE); + freeLineItem.addItemListener(this); + linePopup.add(freeLineItem); + arrowItem = new CheckboxMenuItem("Arrow tool", lineType==LINE&&!arrowMode); + arrowItem.addItemListener(this); + linePopup.add(arrowItem); + add(linePopup); + + zoomPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + zoomPopup.setFont(Menus.getFont()); + addMenuItem(zoomPopup, "Reset Zoom"); + addMenuItem(zoomPopup, "Zoom In"); + addMenuItem(zoomPopup, "Zoom Out"); + addMenuItem(zoomPopup, "View 100%"); + addMenuItem(zoomPopup, "Zoom To Selection"); + addMenuItem(zoomPopup, "Scale to Fit"); + addMenuItem(zoomPopup, "Set..."); + addMenuItem(zoomPopup, "Maximize"); + add(zoomPopup); + + pickerPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + pickerPopup.setFont(Menus.getFont()); + addMenuItem(pickerPopup, "White/Black"); + addMenuItem(pickerPopup, "Black/White"); + addMenuItem(pickerPopup, "Red"); + addMenuItem(pickerPopup, "Green"); + addMenuItem(pickerPopup, "Blue"); + addMenuItem(pickerPopup, "Yellow"); + addMenuItem(pickerPopup, "Cyan"); + addMenuItem(pickerPopup, "Magenta"); + pickerPopup.addSeparator(); + addMenuItem(pickerPopup, "Foreground..."); + addMenuItem(pickerPopup, "Background..."); + addMenuItem(pickerPopup, "Colors..."); + addMenuItem(pickerPopup, "Color Picker..."); + add(pickerPopup); + + switchPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + switchPopup.setFont(Menus.getFont()); + add(switchPopup); + } + + private void addMenuItem(PopupMenu menu, String command) { + MenuItem item = new MenuItem(command); + item.addActionListener(this); + menu.add(item); + } + + /** Returns the ID of the current tool (Toolbar.RECTANGLE, Toolbar.OVAL, etc.). */ + public static int getToolId() { + int id = current; + if (legacyMode) { + if (id==CUSTOM1) + id=UNUSED; + else if (id>=CUSTOM2) + id--; + } + return id; + } + + /** Returns the ID of the tool whose name (the description displayed in the status bar) + starts with the specified string, or -1 if the tool is not found. */ + public int getToolId(String name) { + int tool = -1; + for (int i=0; i1.0) + g2d.setStroke(new BasicStroke(IJ.isMacOSX()?1.4f:1.25f)); + } else + g2d.setStroke(new BasicStroke(scale)); + } + + private void fill3DRect(Graphics g, int x, int y, int width, int height, boolean raised) { + if (null==g) return; + if (raised) + g.setColor(gray); + else + g.setColor(darker); + g.fillRect(x+1, y+1, width-2, height-2); + g.setColor(raised ? brighter : evenDarker); + g.drawLine(x, y, x, y + height - 1); + g.drawLine(x + 1, y, x + width - 2, y); + g.setColor(raised ? evenDarker : brighter); + g.drawLine(x + 1, y + height - 1, x + width - 1, y + height - 1); + g.drawLine(x + width - 1, y, x + width - 1, y + height - 2); + } + + private void drawButton(Graphics g, int tool) { + if (g==null) return; + if (legacyMode) { + if (tool==UNUSED) + tool = CUSTOM1; + else if (tool>=CUSTOM1) + tool++; + if ((tool==POLYLINE && lineType!=POLYLINE) || (tool==FREELINE && lineType!=FREELINE)) + return; + } + int index = toolIndex(tool); + int x = index*buttonWidth + 1*scale; + if (tool>=CUSTOM1) + x -= buttonWidth-gapSize; + if (tool!=UNUSED) + fill3DRect(g, x, 1, buttonWidth, buttonHeight-1, !down[tool]); + g.setColor(toolColor); + x = index*buttonWidth + offset; + if (tool>=CUSTOM1) + x -= buttonWidth-gapSize; + int y = offset; + if (dscale==1.3) {x++; y++;} + if (dscale==1.4) {x+=2; y+=2;} + if (down[tool]) { x++; y++;} + this.g = g; + if (tool>=CUSTOM1 && tool<=getNumTools() && icons[tool]!=null) { + drawIcon(g, tool, x+1*scale, y+1*scale); + return; + } + switch (tool) { + case RECTANGLE: + xOffset = x; yOffset = y; + if (rectType==ROUNDED_RECT_ROI) + g.drawRoundRect(x-1*scale, y+1*scale, 17*scale, 13*scale, 8*scale, 8*scale); + else if (rectType==ROTATED_RECT_ROI) + polyline(0,10,7,0,15,6,8,16,0,10); + else + g.drawRect(x-1*scale, y+1*scale, 17*scale, 13*scale); + drawTriangle(16,16); + return; + case OVAL: + xOffset = x; yOffset = y; + if (ovalType==BRUSH_ROI) { + yOffset = y - 1; + polyline(6,4,8,2,12,1,15,2,16,4,15,7,12,8,9,11,9,14,6,16,2,16,0,13,1,10,4,9,6,7,6,4); + } else if (ovalType==ELLIPSE_ROI) { + xOffset = x - 1; + yOffset = y + 1; + polyline(11,0,13,0,14,1,15,1,16,2,17,3,17,7,12,12,11,12,10,13,8,13,7,14,4,14,3,13,2,13,1,12,1,11,0,10,0,9,1,8,1,7,6,2,7,2,8,1,10,1,11,0); + } else + g.drawOval(x, y+1*scale, 17*scale, 13*scale); + drawTriangle(16,16); + return; + case POLYGON: + xOffset = x+1; yOffset = y+2; + polyline(4,0,15,0,15,1,11,5,11,6,14,11,14,12,0,12,0,4,4,0); + return; + case FREEROI: + xOffset = x; yOffset = y+3; + polyline(2,0,5,0,7,3,10,3,12,0,15,0,17,2,17,5,16,8,13,10,11,11,6,11,4,10,1,8,0,6,0,2,2,0); + return; + case LINE: + if (arrowMode) { + xOffset = x; yOffset = y; + m(1,14); d(14,1); m(6,5); d(14,1); m(10,9); d(14,1); m(6,5); d(10,9); + } else { + xOffset = x-1; yOffset = y-1; + m(1,17); d(18,0); + drawDot(0,16); drawDot(17,0); + } + drawTriangle(16,16); + return; + case POLYLINE: + xOffset = x; yOffset = y; + polyline(15,6,11,2,1,2,1,3,7,9,2,14); + drawTriangle(14,16); + return; + case FREELINE: + xOffset = x; yOffset = y; + polyline(16,4,14,6,12,6,9,3,8,3,6,7,2,11,1,11); + drawTriangle(14,16); + return; + case POINT: + xOffset = x; yOffset = y; + if (multiPointMode) { + drawPoint(1,3); drawPoint(9,0); drawPoint(15,5); + drawPoint(10,11); drawPoint(2,13); + } else { + m(1,8); d(6,8); d(6,6); d(10,6); d(10,10); d(6,10); d(6,9); + m(8,1); d(8,5); m(11,8); d(15,8); m(8,11); d(8,15); + m(8,8); d(8,8); + g.setColor(Roi.getColor()); + m(7,7); d(9,7); + m(7,8); d(9,8); + m(7,9); d(9,9); + } + drawTriangle(16,16); + return; + case WAND: + xOffset = x+2; yOffset = y+1; + dot(4,0); m(2,0); d(3,1); d(4,2); m(0,0); d(1,1); + m(0,2); d(1,3); d(2,4); dot(0,4); m(3,3); d(15,15); + g.setColor(Roi.getColor()); + m(1,2); d(3,2); m(2,1); d(2,3); + return; + case TEXT: + xOffset = x; yOffset = y; + m(1,16); d(9,0); d(16,16); + m(0,16); d(2,16); + m(15,16); d(17,16); + m(4,10); d(13,10); + return; + case MAGNIFIER: + xOffset = x; yOffset = y; + g.drawOval(x+3, y, 13*scale, 13*scale); + m(5,12); d(-1,18); + drawTriangle(15,17); + return; + case HAND: + xOffset = x; yOffset = y; + polyline(5,17,5,16,0,11,0,8,1,8,5,11,5,2,8,2,8,8,8,0,11,0,11,8,11,1,14,1,14,9,14,3,17,3,17,12,16,13,16,17); + return; + case DROPPER: + // draw foreground/background rectangles + g.setColor(backgroundColor); + g.fillRect(x+2*scale, y+3*scale, 15*scale, 16*scale); + g.drawRect(x, y+2*scale, 13*scale, 13*scale); + g.setColor(foregroundColor); + g.fillRect(x, y+2*scale, 13*scale, 13*scale); + // draw dropper icon + xOffset = x+3; yOffset = y-4; + g.setColor(toolColor); + m(12,2); d(14,2); + m(11,3); d(15,3); + m(11,4); d(15,4); + m(8,5); d(15,5); + m(9,6); d(14,6); + polyline(10,7,12,7,12,9); + polyline(9,6,2,13,2,15,4,15,11,8); + g.setColor(gray); + polygon(9,6,2,13,2,15,4,15,11,8); + drawTriangle(12,21); + return; + case ANGLE: + xOffset = x; yOffset = y+3; + m(0,11); d(13,-1); m(0,11); d(16,11); + m(10,11); d(10,8); m(9,7); d(9,6); dot(8,5); + drawDot(13,-2); drawDot(16,10); + return; + } + } + + void drawTriangle(int x, int y) { + g.setColor(triangleColor); + xOffset+=x*scale; yOffset+=y*scale; + m(0,0); d(4,0); m(1,1); d(3,1); m(2,2); d(2,2); + } + + void drawDot(int x, int y) { + g.fillRect(xOffset+x*scale, yOffset+y*scale, 2*scale, 2*scale); + } + + void drawPoint(int x, int y) { + g.setColor(toolColor); + m(x-3,y); d(x+3,y); + m(x,y-3); d(x,y+3); + g.setColor(Roi.getColor()); + dot(x,y); dot(x-1,y-1); + } + + void drawIcon(Graphics g, int tool, int x, int y) { + if (null==g) return; + icon = icons[tool]; + if (icon==null) return; + this.icon = icon; + int x1, y1, x2, y2; + pc = 0; + if (icon.trim().startsWith("icon:")) { + String path = IJ.getDir("macros")+"toolsets/icons/"+icon.substring(icon.indexOf(":")+1); + try { + BufferedImage bi = ImageIO.read(new File(path)); + if (scale==1) + g.drawImage(bi,x-5,y-5,null); + else { + int size = Math.max(bi.getWidth(),bi.getHeight()); + g.drawImage(bi,x-5*scale,y-5*scale, size*scale,size*scale,null); + } + } catch (Exception e) { + IJ.error("Toolbar", "Error reading tool icon:\n"+path); + } + } else { + while (true) { + char command = icon.charAt(pc++); + if (pc>=icon.length()) break; + switch (command) { + case 'B': x+=v(); y+=v(); break; // adjust base + case 'N': x-=v(); y-=v(); break; // adjust base negatively + case 'R': g.drawRect(x+v(), y+v(), v(), v()); break; // rectangle + case 'F': g.fillRect(x+v(), y+v(), v(), v()); break; // filled rectangle + case 'O': g.drawOval(x+v(), y+v(), v(), v()); break; // oval + case 'V': case 'o': g.fillOval(x+v(), y+v(), v(), v()); break; // filled oval + case 'C': // set color + int saveScale = scale; + scale = 1; + int v1=v(), v2=v(), v3=v(); + int red=v1*16, green=v2*16, blue=v3*16; + if (red>255) red=255; if (green>255) green=255; if (blue>255) blue=255; + Color color = v1==1&&v2==2&&v3==3?foregroundColor:new Color(red,green,blue); + g.setColor(color); + scale = saveScale; + break; + case 'L': g.drawLine(x+v(), y+v(), x+v(), y+v()); break; // line + case 'D': g.fillRect(x+v(), y+v(), scale, scale); break; // dot + case 'P': // polyline + Polygon p = new Polygon(); + p.addPoint(x+v(), y+v()); + while (true) { + x2=v(); if (x2==0) break; + y2=v(); if (y2==0) break; + p.addPoint(x+x2, y+y2); + } + g.drawPolyline(p.xpoints, p.ypoints, p.npoints); + break; + case 'G': case 'H':// polygon or filled polygon + p = new Polygon(); + p.addPoint(x+v(), y+v()); + while (true) { + x2=v(); y2=v(); + if (x2==0 && y2==0 && p.npoints>2) + break; + p.addPoint(x+x2, y+y2); + } + if (command=='G') + g.drawPolygon(p.xpoints, p.ypoints, p.npoints); + else + g.fillPolygon(p.xpoints, p.ypoints, p.npoints); + break; + case 'T': // text (one character) + x2 = x+v()-2; + y2 = y+v(); + int size = v()*10+v()+1; + char[] c = new char[1]; + c[0] = pc=icon.length()) break; + } + } + if (menus[tool]!=null && menus[tool].getItemCount()>0) { + xOffset = x; yOffset = y; + drawTriangle(15, 16); + } + } + + int v() { + if (pc>=icon.length()) return 0; + char c = icon.charAt(pc++); + switch (c) { + case '0': return 0*scale; + case '1': return 1*scale; + case '2': return 2*scale; + case '3': return 3*scale; + case '4': return 4*scale; + case '5': return 5*scale; + case '6': return 6*scale; + case '7': return 7*scale; + case '8': return 8*scale; + case '9': return 9*scale; + case 'a': return 10*scale; + case 'b': return 11*scale; + case 'c': return 12*scale; + case 'd': return 13*scale; + case 'e': return 14*scale; + case 'f': return 15*scale; + case 'g': return 16*scale; + case 'h': return 17*scale; + case 'i': return 18*scale; + case 'j': return 19*scale; + case 'k': return 20*scale; + case 'l': return 21*scale; + case 'm': return 22*scale; + case 'n': return 23*scale; + default: return 0; + } + } + + private void showMessage(int tool) { + if (IJ.statusBarProtected()) + return; + if (tool>=UNUSED && tool=UNUSED && current=CUSTOM1) + tool++; + if (IJ.debugMode) IJ.log("Toolbar.setTool: "+tool); + if ((tool==current&&!(tool==RECTANGLE||tool==OVAL||tool==POINT)) || tool<0 || tool>=getNumTools()-1) + return; + if (tool>=CUSTOM1&&tool<=getNumTools()-2) { + if (names[tool]==null) + names[tool] = "Spare tool"; // enable tool + if (names[tool].indexOf("Action Tool")!=-1) + return; + } + if (isLine(tool)) lineType = tool; + setTool2(tool); + } + + private void setTool2(int tool) { + if (!isValidTool(tool)) return; + String previousName = getToolName(); + previousTool = current; + current = tool; + Graphics g = this.getGraphics(); + if (g==null) + return; + down[current] = true; + if (current!=previousTool) + down[previousTool] = false; + Graphics2D g2d = (Graphics2D)g; + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + setStrokeWidth(g2d); + drawButton(g, previousTool); + drawButton(g, current); + if (null==g) return; + g.dispose(); + showMessage(current); + if (Recorder.record) { + String name = getName(current); + if (name!=null && name.equals("dropper")) disableRecording=true; + if (name!=null && !disableRecording) { + IJ.wait(100); // workaround for OSX/Java 8 bug + Recorder.record("setTool", name); + } + if (name!=null && !name.equals("dropper")) disableRecording=false; + } + if (legacyMode) + repaint(); + if (!previousName.equals(getToolName())) { + IJ.notifyEventListeners(IJEventListener.TOOL_CHANGED);; + repaint(); + } + } + + boolean isValidTool(int tool) { + if (tool<0 || tool>=getNumTools()) + return false; + if (tool>=CUSTOM1 && tool=0) { + int v = (int)value; + if (v>255) v=255; + setForegroundColor(new Color(v,v,v)); + } + foregroundValue = value; + } + + public static double getBackgroundValue() { + return backgroundValue; + } + + public static void setBackgroundValue(double value) { + if (value>=0) { + int v = (int)value; + if (v>255) v=255; + setBackgroundColor(new Color(v,v,v)); + } + backgroundValue = value; + } + + private static void setRoiColor(Color c) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) return; + Roi roi = imp.getRoi(); + if (roi!=null && (roi.isDrawingTool())) { + roi.setStrokeColor(c); + imp.draw(); + } + } + + /** Returns the size of the selection brush tool, or 0 if the brush tool is not enabled. */ + public static int getBrushSize() { + if (ovalType==BRUSH_ROI) + return brushSize; + else + return 0; + } + + /** Set the size of the selection brush tool, in pixels. */ + public static void setBrushSize(int size) { + brushSize = size; + if (brushSize<1) brushSize = 1; + Prefs.set(BRUSH_SIZE, brushSize); + } + + /** Returns the rounded rectangle arc size, or 0 if the rounded rectangle tool is not enabled. */ + public static int getRoundRectArcSize() { + if (rectType==ROUNDED_RECT_ROI) + return arcSize; + else + return 0; + } + + /** Sets the rounded rectangle corner diameter (pixels). */ + public static void setRoundRectArcSize(int size) { + if (size<=0) + rectType = RECT_ROI; + else { + arcSize = size; + Prefs.set(CORNER_DIAMETER, arcSize); + } + repaintTool(RECTANGLE); + ImagePlus imp = WindowManager.getCurrentImage(); + Roi roi = imp!=null?imp.getRoi():null; + if (roi!=null && roi.getType()==Roi.RECTANGLE) + roi.setCornerDiameter(rectType==ROUNDED_RECT_ROI?arcSize:0); + } + + /** Returns 'true' if the multi-point tool is enabled. */ + public static boolean getMultiPointMode() { + return multiPointMode; + } + + /** Returns the rectangle tool type (RECT_ROI, ROUNDED_RECT_ROI or ROTATED_RECT_ROI). */ + public static int getRectToolType() { + return rectType; + } + + /** Returns the oval tool type (OVAL_ROI, ELLIPSE_ROI or BRUSH_ROI). */ + public static int getOvalToolType() { + return ovalType; + } + + /** Returns the button width (button spacing). */ + public static int getButtonSize() { + return buttonWidth; + } + + public static void repaintTool(int tool) { + Toolbar tb = getInstance(); + if (tb!=null) { + Graphics g = tb.getGraphics(); + if (IJ.debugMode) IJ.log("Toolbar.repaintTool: "+tool+" "+g); + if (g==null) return; + if (dscale>1.0) + tb.setStrokeWidth((Graphics2D)g); + ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + tb.drawButton(g, tool); + if (g!=null) g.dispose(); + } + } + + // Returns the toolbar position index of the specified tool + int toolIndex(int tool) { + switch (tool) { + case RECTANGLE: return 0; + case OVAL: return 1; + case POLYGON: return 2; + case FREEROI: return 3; + case LINE: return 4; + case POLYLINE: return 4; + case FREELINE: return 4; + case POINT: return 6; + case WAND: return 7; + case TEXT: return 8; + case MAGNIFIER: return 9; + case HAND: return 10; + case DROPPER: return 11; + case ANGLE: return 5; + case UNUSED: return 12; + default: return tool - 2; + } + } + + // Returns the tool corresponding to the specified x coordinate + private int toolID(int x) { + if (x>buttonWidth*12+gapSize) + x -= gapSize; + int index = x/buttonWidth; + switch (index) { + case 0: return RECTANGLE; + case 1: return OVAL; + case 2: return POLYGON; + case 3: return FREEROI; + case 4: return lineType; + case 5: return ANGLE; + case 6: return POINT; + case 7: return WAND; + case 8: return TEXT; + case 9: return MAGNIFIER; + case 10: return HAND; + case 11: return DROPPER; + default: return index + 3; + } + } + + private boolean inGap(int x) { + return x>=(buttonWidth*12) && x<(buttonWidth*12+gapSize); + } + + public void triggerPopupMenu(int newTool, MouseEvent e, boolean isRightClick, boolean isLongPress) { + mpPrevious = current; + if (isMacroTool(newTool)) { + String name = names[newTool]; + if (newTool==UNUSED || name.contains("Unused Tool")) + return; + if (name.indexOf("Action Tool")!=-1) { + if (e.isPopupTrigger()||e.isMetaDown()) { + name = name.endsWith(" ")?name:name+" "; + tools[newTool].runMacroTool(name+"Options"); + } else { + //drawTool(newTool, true); + //IJ.wait(50); + drawTool(newTool, false); + runMacroTool(newTool); + } + return; + } else { + name = name.endsWith(" ")?name:name+" "; + tools[newTool].runMacroTool(name+"Selected"); + } + } + if (!isLongPress) + setTool2(newTool); + int x = e.getX(); + int y = e.getY(); + if (current==RECTANGLE && isRightClick) { + rectItem.setState(rectType==RECT_ROI); + roundRectItem.setState(rectType==ROUNDED_RECT_ROI); + rotatedRectItem.setState(rectType==ROTATED_RECT_ROI); + if (IJ.isMacOSX()) IJ.wait(10); + rectPopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (current==OVAL && isRightClick) { + ovalItem.setState(ovalType==OVAL_ROI); + ellipseItem.setState(ovalType==ELLIPSE_ROI); + brushItem.setState(ovalType==BRUSH_ROI); + if (IJ.isMacOSX()) IJ.wait(10); + ovalPopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (current==POINT && isRightClick) { + pointItem.setState(!multiPointMode); + multiPointItem.setState(multiPointMode); + if (IJ.isMacOSX()) IJ.wait(10); + pointPopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (isLine(current) && isRightClick) { + straightLineItem.setState(lineType==LINE&&!arrowMode); + polyLineItem.setState(lineType==POLYLINE); + freeLineItem.setState(lineType==FREELINE); + arrowItem.setState(lineType==LINE&&arrowMode); + if (IJ.isMacOSX()) IJ.wait(10); + linePopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (current==MAGNIFIER && isRightClick) { + zoomPopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (current==DROPPER && isRightClick) { + pickerPopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (isMacroTool(current) && isRightClick) { + String name = names[current].endsWith(" ")?names[current]:names[current]+" "; + tools[current].runMacroTool(name+"Options"); + } + if (isPlugInTool(current) && isRightClick) { + tools[current].showPopupMenu(e, this); + } + } + + public void mousePressed(final MouseEvent e) { + int x = e.getX(); + if (inGap(x)) + return; + final int newTool = toolID(x); + if (newTool==getNumTools()-1) { + showSwitchPopupMenu(e); + return; + } + if (!isValidTool(newTool)) + return; + if (menus[newTool]!=null && menus[newTool].getItemCount()>0) { + menus[newTool].show(e.getComponent(), e.getX(), e.getY()); + return; + } + int flags = e.getModifiers(); + boolean isRightClick = e.isPopupTrigger()||(!IJ.isMacintosh()&&(flags&Event.META_MASK)!=0); + boolean doubleClick = newTool==current && (System.currentTimeMillis()-mouseDownTime)<=DOUBLE_CLICK_THRESHOLD; + mouseDownTime = System.currentTimeMillis(); + if (!doubleClick || isRightClick) { + triggerPopupMenu(newTool, e, isRightClick, false); + if (isRightClick) mouseDownTime = 0L; + } else if(!isRightClick) { //double click + if (isMacroTool(current)) { + String name = names[current].endsWith(" ")?names[current]:names[current]+" "; + tools[current].runMacroTool(name+"Options"); + return; + } + if (isPlugInTool(current)) { + tools[current].showOptionsDialog(); + return; + } + ImagePlus imp = WindowManager.getCurrentImage(); + switch (current) { + case RECTANGLE: + if (rectType==ROUNDED_RECT_ROI) + IJ.doCommand("Rounded Rect Tool..."); + else + IJ.doCommand("Roi Defaults..."); + break; + case OVAL: + showBrushDialog(); + break; + case MAGNIFIER: + if (imp!=null) { + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) ic.unzoom(); + } + break; + case LINE: case POLYLINE: case FREELINE: + if (current==LINE && arrowMode) { + IJ.doCommand("Arrow Tool..."); + } else + IJ.runPlugIn("ij.plugin.frame.LineWidthAdjuster", ""); + break; + case ANGLE: + showAngleDialog(); + break; + case POINT: + IJ.doCommand("Point Tool..."); + break; + case WAND: + IJ.doCommand("Wand Tool..."); + break; + case TEXT: + IJ.run("Fonts..."); + break; + case DROPPER: + IJ.doCommand("Color Picker..."); + setTool2(mpPrevious); + break; + default: + } + } + + if (!isRightClick && longClickDelay>0) { + if (pressTimer==null) + pressTimer = new Timer(); + pressTimer.schedule(new TimerTask() { + public void run() { + if (pressTimer != null) { + pressTimer.cancel(); + pressTimer = null; + } + triggerPopupMenu(newTool, e, true, true); + } + }, longClickDelay); + } + + } + + public void mouseReleased(MouseEvent e) { + if (pressTimer!=null) { + pressTimer.cancel(); + pressTimer = null; + } + } + + void showSwitchPopupMenu(MouseEvent e) { + String path = IJ.getDir("macros")+"toolsets/"; + if (path==null) + return; + boolean applet = IJ.getApplet()!=null; + File f = new File(path); + String[] list; + if (!applet && f.exists() && f.isDirectory()) { + list = f.list(); + if (list==null) return; + Arrays.sort(list); + } else + list = new String[0]; + switchPopup.removeAll(); + path = IJ.getDir("macros") + "StartupMacros.txt"; + f = new File(path); + if (!f.exists()) { + path = IJ.getDir("macros") + "StartupMacros.ijm"; + f = new File(path); + } + if (!applet && f.exists()) + addItem("Startup Macros"); + else + addItem("StartupMacros*"); + for (int i=0; i=5) + pluginsMenu = menuBar.getMenu(5); + if (pluginsMenu==null || !"Plugins".equals(pluginsMenu.getLabel())) + return; + n = pluginsMenu.getItemCount(); + Menu toolsMenu = null; + for (int i=0; i=CUSTOM1 && tool=CUSTOM1 && toolTools submenu.\n"+ + " \n"+ + "Hold the shift key down while selecting a\n"+ + "toolset to view its source code.\n"+ + " \n"+ + "More macro toolsets are available at\n"+ + " <"+IJ.URL+"/macros/toolsets/>\n"+ + " \n"+ + "Plugin tools can be downloaded from\n"+ + "the Tools section of the Plugins page at\n"+ + " <"+IJ.URL+"/plugins/>\n" + ); + return; + } else if (label.endsWith("*")) { + // load from ij.jar + MacroInstaller mi = new MacroInstaller(); + label = label.substring(0, label.length()-1) + ".txt"; + path = "/macros/"+label; + if (IJ.shiftKeyDown()) + showCode(label, mi.openFromIJJar(path)); + else { + resetTools(); + mi.installFromIJJar(path); + } + } else { + // load from ImageJ/macros/toolsets + if (label.equals("Startup Macros")) { + installStartupMacros(); + return; + } else if (label.endsWith(" ")) + path = IJ.getDir("macros")+"toolsets"+File.separator+label.substring(0, label.length()-1)+".ijm"; + else + path = IJ.getDir("macros")+"toolsets"+File.separator+label+".txt"; + try { + if (IJ.shiftKeyDown()) { + IJ.open(path); + IJ.setKeyUp(KeyEvent.VK_SHIFT); + } else + new MacroInstaller().run(path); + } catch(Exception ex) {} + } + } + } + + private void removeTools() { + removeMacroTools(); + setTool(RECTANGLE); + currentSet = "Startup Macros"; + resetPrefs(); + if (nExtraTools>0) { + String name = names[getNumTools()-1]; + String icon = icons[getNumTools()-1]; + nExtraTools = 0; + names[getNumTools()-1] = name; + icons[getNumTools()-1] = icon; + ps = new Dimension(buttonWidth*NUM_BUTTONS-(BUTTON_WIDTH-gapSize)+nExtraTools*BUTTON_WIDTH, buttonHeight); + IJ.getInstance().pack(); + } + } + + private void resetPrefs() { + for (int i=0; i<7; i++) { + String key = TOOL_KEY+(i/10)%10+i%10; + if (!Prefs.get(key, "").equals("")) + Prefs.set(key, ""); + } + } + + public static void restoreTools() { + Toolbar tb = Toolbar.getInstance(); + if (tb!=null) { + if (tb.getToolId()>=UNUSED) + tb.setTool(RECTANGLE); + tb.installStartupMacros(); + } + } + + private void installStartupMacros() { + resetTools(); + String path = IJ.getDir("macros")+"StartupMacros.txt"; + File f = new File(path); + if (!f.exists()) { + path = IJ.getDir("macros")+"StartupMacros.ijm"; + f = new File(path); + } + if (!f.exists()) { + path = IJ.getDir("macros")+"StartupMacros.fiji.ijm"; + f = new File(path); + } + if (!f.exists()) { + IJ.error("StartupMacros not found in\n \n"+IJ.getDir("macros")); + return; + } + if (IJ.shiftKeyDown()) { + IJ.open(path); + IJ.setKeyUp(KeyEvent.VK_SHIFT); + } else { + try { + MacroInstaller mi = new MacroInstaller(); + mi.installFile(path); + } catch + (Exception ex) {} + } + } + + public void actionPerformed(ActionEvent e) { + MenuItem item = (MenuItem)e.getSource(); + String cmd = e.getActionCommand(); + PopupMenu popup = (PopupMenu)item.getParent(); + + if (zoomPopup==popup) { + if ("Zoom In".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "in"); + else if ("Zoom Out".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "out"); + else if ("Reset Zoom".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "orig"); + else if ("View 100%".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "100%"); + else if ("Zoom To Selection".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "to"); + else if ("Scale to Fit".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "scale"); + else if ("Set...".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "set"); + else if ("Maximize".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "max"); + disableRecording = true; + setTool(previousTool); + disableRecording = false; + return; + } + + if (pickerPopup==popup) { + if ("White/Black".equals(cmd)) { + setAndRecordForgroundColor(Color.white); + setAndRecordBackgroundColor(Color.black); + } else if ("Black/White".equals(cmd)) { + setAndRecordForgroundColor(Color.black); + setAndRecordBackgroundColor(Color.white); + } else if ("Red".equals(cmd)) + setAndRecordForgroundColor(Color.red); + else if ("Green".equals(cmd)) + setAndRecordForgroundColor(Color.green); + else if ("Blue".equals(cmd)) + setAndRecordForgroundColor(Color.blue); + else if ("Yellow".equals(cmd)) + setAndRecordForgroundColor(Color.yellow); + else if ("Cyan".equals(cmd)) + setAndRecordForgroundColor(Color.cyan); + else if ("Magenta".equals(cmd)) + setAndRecordForgroundColor(Color.magenta); + else if ("Foreground...".equals(cmd)) + setAndRecordForgroundColor(new ColorChooser("Select Foreground Color", foregroundColor, false).getColor()); + else if ("Background...".equals(cmd)) + setAndRecordBackgroundColor(new ColorChooser("Select Background Color", backgroundColor, false).getColor()); + else if ("Colors...".equals(cmd)) { + IJ.run("Colors...", ""); + Recorder.setForegroundColor(getForegroundColor()); + Recorder.setBackgroundColor(getBackgroundColor()); + } else + IJ.run("Color Picker...", ""); + if (!"Color Picker".equals(cmd)) + ColorPicker.update(); + setTool(previousTool); + return; + } + + int tool = -1; + for (int i=CUSTOM1; i=0 && (toolTip.length()-index)>4; + int tool =-1; + for (int i=CUSTOM1; i<=getNumTools()-2; i++) { + if (names[i]==null || toolTip.startsWith(names[i])) { + tool = i; + break; + } + } + if (tool==CUSTOM1) + legacyMode = toolTip.startsWith("Select and Transform Tool"); //TrakEM2 + if (tool==-1 && (nExtraTools0 && toolTip.charAt(index-1)==' ') + names[tool] = toolTip.substring(0, index-1); + else + names[tool] = toolTip.substring(0, index); + } else { + if (toolTip.endsWith("-")) + toolTip = toolTip.substring(0, toolTip.length()-1); + else if (toolTip.endsWith("- ")) + toolTip = toolTip.substring(0, toolTip.length()-2); + names[tool] = toolTip; + } + if (tool==current && (names[tool].indexOf("Action Tool")!=-1||names[tool].indexOf("Unused Tool")!=-1)) + setTool(RECTANGLE); + if (names[tool].endsWith(" Menu Tool")) + installMenu(tool); + if (IJ.debugMode) IJ.log("Toolbar.addTool: "+tool+" "+toolTip); + return tool; + } + + void installMenu(int tool) { + Program pgm = macroInstaller.getProgram(); + Hashtable h = pgm.getMenus(); + if (h==null) return; + String[] commands = (String[])h.get(names[tool]); + if (commands==null) + return; + if (menus[tool]==null) { + menus[tool] = new PopupMenu(""); + if (Menus.getFontSize()!=0) + menus[tool].setFont(Menus.getFont()); + add(menus[tool] ); + } else + menus[tool].removeAll(); + for (int i=0; i0 || custom1Name==null) + setPrefs(tool); + } + } + + private void setPrefs(int id) { + if (doNotSavePrefs) + return; + boolean ok = isBuiltInTool(names[id]); + String prefsName = instance.names[id]; + if (!ok) { + String name = names[id]; + int i = name.indexOf(" ("); // remove any hint in parens + if (i>0) { + name = name.substring(0, i); + prefsName=name; + } + } + int index = id - CUSTOM1; + String key = TOOL_KEY + (index/10)%10 + index%10; + Prefs.set(key, prefsName); + } + + private boolean isBuiltInTool(String name) { + for (int i=0; i=CUSTOM1) + instance.setTool(RECTANGLE); + instance.resetTools(); + instance.repaint(); + } + } + + /** Adds a plugin tool to the first available toolbar slot, + or to the last slot if the toolbar is full. */ + public static void addPlugInTool(PlugInTool tool) { + if (instance==null) return; + String nameAndIcon = tool.getToolName()+" - "+tool.getToolIcon(); + instance.addingSingleTool = true; + int id = instance.addTool(nameAndIcon); + instance.addingSingleTool = false; + if (id!=-1) { + instance.tools[id] = tool; + if (instance.menus[id]!=null) + instance.menus[id].removeAll(); + instance.repaintTool(id); + if (!instance.installingStartupTool) + instance.setTool(id); + else + instance.installingStartupTool = false; + instance.setPrefs(id); + } + } + + public static PlugInTool getPlugInTool() { + PlugInTool tool = null; + if (instance==null) + return null; + if (current2; + return rtn; + } + + public static boolean installStartupMacrosTools() { + String customTool0 = Prefs.get(Toolbar.TOOL_KEY+"00", ""); + return customTool0.equals("") || Character.isDigit(customTool0.charAt(0)); + } + + public int getNumTools() { + return NUM_TOOLS + nExtraTools; + } + + /** Sets the tool menu long click delay in milliseconds + * (default is 600). Set to 0 to disable long click triggering. + */ + public static void setLongClickDelay(int delay) { + longClickDelay = delay; + } + + /** Sets the icon of the specified macro or plugin tool.
+ * See: Help>Examples>Tool>Animated Icon Tool; + */ + public static void setIcon(String toolName, String icon) { + if (instance==null) + return; + int tool = 0; + for (int i=CUSTOM1; i0) { + instance.icons[tool] = icon; + Graphics2D g = (Graphics2D)instance.getGraphics(); + instance.setStrokeWidth(g); + instance.drawButton(g, tool); + } + } + +} diff --git a/src/ij/gui/TrimmedButton.java b/src/ij/gui/TrimmedButton.java new file mode 100644 index 0000000..7949b80 --- /dev/null +++ b/src/ij/gui/TrimmedButton.java @@ -0,0 +1,28 @@ +package ij.gui; +import java.awt.*; +import javax.swing.*; + +/** This is an extended Button class used to reduce the width of the HUGE buttons on Mac OS X. */ +public class TrimmedButton extends Button { + private int trim = 0; + + public TrimmedButton(String title, int trim) { + super(title); + if (trim>0) { + LookAndFeel laf = UIManager.getLookAndFeel(); + String name = laf!=null?laf.getName():""; + if (ij.IJ.isMacOSX() && name!=null && !name.equals("Mac OS X")) + trim = 0; + } + this.trim = trim; + } + + public Dimension getMinimumSize() { + return new Dimension(super.getMinimumSize().width-trim, super.getMinimumSize().height); + } + + public Dimension getPreferredSize() { + return getMinimumSize(); + } + +} diff --git a/src/ij/gui/WaitForUserDialog.java b/src/ij/gui/WaitForUserDialog.java new file mode 100644 index 0000000..31a758c --- /dev/null +++ b/src/ij/gui/WaitForUserDialog.java @@ -0,0 +1,126 @@ +package ij.gui; +import ij.*; +import ij.plugin.frame.RoiManager; +import java.awt.*; +import java.awt.event.*; +import java.lang.reflect.*; + + +/** +* This is a non-modal dialog box used to ask the user to perform some task +* while a macro or plugin is running. It implements the waitForUser() macro +* function. It is based on Michael Schmid's Wait_For_User plugin.
+* Example: +* new WaitForUserDialog("Use brush to draw on overlay").show(); +*/ +public class WaitForUserDialog extends Dialog implements ActionListener, KeyListener { + protected Button button; + protected Button cancelButton; + protected MultiLineLabel label; + static protected int xloc=-1, yloc=-1; + private boolean escPressed; + + public WaitForUserDialog(String title, String text) { + super(IJ.getInstance(), title, false); + IJ.protectStatusBar(false); + if (text!=null && text.startsWith("IJ: ")) + text = text.substring(4); + label = new MultiLineLabel(text, 175); + if (!IJ.isLinux()) label.setFont(new Font("SansSerif", Font.PLAIN, 14)); + if (IJ.isMacOSX()) { + RoiManager rm = RoiManager.getInstance(); + if (rm!=null) rm.runCommand("enable interrupts"); + } + GridBagLayout gridbag = new GridBagLayout(); //set up the layout + GridBagConstraints c = new GridBagConstraints(); + setLayout(gridbag); + c.insets = new Insets(6, 6, 0, 6); + c.gridx = 0; c.gridy = 0; c.anchor = GridBagConstraints.WEST; + add(label,c); + + button = new Button(" OK "); + button.addActionListener(this); + button.addKeyListener(this); + c.insets = new Insets(2, 6, 6, 6); + c.gridx = 0; c.gridy = 1; c.anchor = GridBagConstraints.EAST; + add(button, c); + + if (IJ.isMacro()) { + cancelButton = new Button(" Cancel "); + cancelButton.addActionListener(this); + cancelButton.addKeyListener(this); + c.anchor = GridBagConstraints.WEST; //same as OK button but WEST + add(cancelButton, c); + } + + setResizable(false); + addKeyListener(this); + GUI.scale(this); + pack(); + if (xloc==-1) + GUI.centerOnImageJScreen(this); + else + setLocation(xloc, yloc); + setAlwaysOnTop(true); + } + + public WaitForUserDialog(String text) { + this("Action Required", text); + } + + public void show() { + super.show(); + synchronized(this) { //wait for OK + try {wait();} + catch(InterruptedException e) {return;} + } + } + + public void close() { + synchronized(this) { notify(); } + xloc = getLocation().x; + yloc = getLocation().y; + dispose(); + } + + public void actionPerformed(ActionEvent e) { + String s = e.getActionCommand(); + if(s.indexOf("Cancel") >= 0){ + escPressed = true; + } + close(); + } + + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyDown(keyCode); + if (keyCode==KeyEvent.VK_ENTER || keyCode==KeyEvent.VK_ESCAPE) { + escPressed = keyCode==KeyEvent.VK_ESCAPE; + close(); + } + } + + public boolean escPressed() { + return escPressed; + } + + public void keyReleased(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyUp(keyCode); + } + + public void keyTyped(KeyEvent e) {} + + /** Returns a reference to the 'OK' button */ + public Button getButton() { + return button; + } + + /** Display the next WaitForUser dialog at the specified location. */ + public static void setNextLocation(int x, int y) { + xloc = x; + yloc = y; + } + + +} diff --git a/src/ij/gui/Wand.java b/src/ij/gui/Wand.java new file mode 100644 index 0000000..c6c79b6 --- /dev/null +++ b/src/ij/gui/Wand.java @@ -0,0 +1,353 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.plugin.WandToolOptions; +import java.awt.*; + +/** This class implements ImageJ's wand (tracing) tool. + * The wand selects pixels of equal or similar value or thresholded pixels + * forming a contiguous area. + * The wand creates selections that have only one boundary line (inner holes + * are not excluded from the selection). There may be holes at the boundary, + * however, if the boundary line touches the same vertex twice (both in + * 4-connected and 8-connected mode). + * + * Version 2009-06-01 (code refurbished; tolerance, 4- & 8-connected options added) + */ +public class Wand { + /** Wand operation type: trace outline of 4-connected pixels */ + public final static int FOUR_CONNECTED = 4; + /** Wand operation type: trace outline of 8-connected pixels */ + public final static int EIGHT_CONNECTED = 8; + /** Wand operation type similar to that of ImageJ 1.42p and before; for backwards + * compatibility. + * In this mode, no checking is done whether the foreground or the background + * gets selected; four- or 8-connected behaviour depends on foreground/background + * and (if no selection) on whether the initial pixel is on a 1-pixel wide line. */ + public final static int LEGACY_MODE = 1; + /** The number of points in the generated outline. */ + public int npoints; + private int maxPoints = 1000; // will be increased if necessary + /** The x-coordinates of the points in the outline. + A vertical boundary at x separates the pixels at x-1 and x. */ + public int[] xpoints = new int[maxPoints]; + /** The y-coordinates of the points in the outline. + A horizontal boundary at y separates the pixels at y-1 and y. */ + public int[] ypoints = new int[maxPoints]; + + private final static int THRESHOLDED_MODE = 256; //work on threshold + private ImageProcessor ip; + private byte[] bpixels; + private int[] cpixels; + private short[] spixels; + private float[] fpixels; + private int width, height; + private float lowerThreshold, upperThreshold; + private int xmin; // of selection created + private boolean exactPixelValue; // For color, match RGB, not gray value + private static boolean allPoints; // output contains intermediate points + + + /** Constructs a Wand object from an ImageProcessor. */ + public Wand(ImageProcessor ip) { + this.ip = ip; + Object pixels = ip.getPixels(); + if (pixels instanceof byte[]) + bpixels = (byte[])pixels; + else if (pixels instanceof int[]) + cpixels = (int[])pixels; + else if (pixels instanceof short[]) + spixels = (short[])pixels; + else if (pixels instanceof float[]) + fpixels = (float[])pixels; + width = ip.getWidth(); + height = ip.getHeight(); + } + + + + /** Traces an object defined by lower and upper threshold values. + * 'mode' can be FOUR_CONNECTED or EIGHT_CONNECTED. + * ('LEGACY_MODE' is also supported and may result in selection of + * interior holes instead of the thresholded area if one clicks left + * of an interior hole). + * The start coordinates must be inside the area or left of it. + * When successful, npoints>0 and the boundary points can be accessed + * in the public xpoints and ypoints fields. */ + public void autoOutline(int startX, int startY, double lower, double upper, int mode) { + lowerThreshold = (float)lower; + upperThreshold = (float)upper; + autoOutline(startX, startY, 0.0, mode|THRESHOLDED_MODE); + } + + /** Traces an object defined by lower and upper threshold values or an + * interior hole; whatever is found first ('legacy mode'). + * For compatibility with previous versions of ImageJ. + * The start coordinates must be inside the area or left of it. + * When successful, npoints>0 and the boundary points can be accessed + * in the public xpoints and ypoints fields. */ + public void autoOutline(int startX, int startY, double lower, double upper) { + autoOutline(startX, startY, lower, upper, THRESHOLDED_MODE|LEGACY_MODE); + } + + /** This is a variation of legacy autoOutline that uses int threshold arguments. */ + public void autoOutline(int startX, int startY, int lower, int upper) { + autoOutline(startX, startY, (double)lower, (double)upper, THRESHOLDED_MODE|LEGACY_MODE); + } + + /** Traces the boundary of an area of uniform color, where + * 'startX' and 'startY' are somewhere inside the area. + * When successful, npoints>0 and the boundary points can be accessed + * in the public xpoints and ypoints fields. + * For compatibility with previous versions of ImageJ only; otherwise + * use the reliable method specifying 4-connected or 8-connected mode + * and the tolerance. */ + public void autoOutline(int startX, int startY) { + autoOutline(startX, startY, 0.0, LEGACY_MODE); + } + + /** Traces the boundary of the area with pixel values within + * 'tolerance' of the value of the pixel at the starting location. + * 'tolerance' is in uncalibrated units. + * 'mode' can be FOUR_CONNECTED or EIGHT_CONNECTED. + * Mode LEGACY_MODE is for compatibility with previous versions of ImageJ; + * ignored if tolerance > 0. + * Mode bit THRESHOLDED_MODE for internal use only; it is set by autoOutline + * with 'upper' and 'lower' arguments. + * When successful, npoints>0 and the boundary points can be accessed + * in the public xpoints and ypoints fields. */ + public void autoOutline(int startX, int startY, double tolerance, int mode) { + if (startX<0 || startX>=width || startY<0 || startY>=height) return; + if (fpixels!=null && Float.isNaN(getPixel(startX, startY))) return; + WandToolOptions.setStart(startX, startY); + exactPixelValue = tolerance==0; + boolean thresholdMode = (mode & THRESHOLDED_MODE) != 0; + boolean legacyMode = (mode & LEGACY_MODE) != 0 && tolerance == 0; + if (!thresholdMode) { + double startValue = getPixel(startX, startY); + lowerThreshold = (float)(startValue - tolerance); + upperThreshold = (float)(startValue + tolerance); + } + int x = startX; + int y = startY; + int seedX; // the first inside pixel + if (inside(x,y)) { // find a border when coming from inside + seedX = x; // (seedX, startY) is an inside pixel + do {x++;} while (inside(x,y)); + } else { // find a border when coming from outside (thresholded only) + do { + x++; + if (x>=width) return; // no border found + } while (!inside(x,y)); + seedX = x; + } + boolean fourConnected; + if (legacyMode) + fourConnected = !thresholdMode && !(isLine(x, y)); + else + fourConnected = (mode & FOUR_CONNECTED) != 0; + //now, we have a border between (x-1, y) and (x,y) + boolean first = true; + while (true) { // loop until we have not traced an inner hole + boolean insideSelected = traceEdge(x, y, fourConnected); + if (legacyMode) return; // in legacy mode, don't care what we have got + if (insideSelected) { // not an inner hole + if (first) return; // started at seed, so we got it (sucessful) + if (xmin<=seedX) { // possibly the correct particle + Polygon poly = new Polygon(xpoints, ypoints, npoints); + if (poly.contains(seedX, startY)) + return; // successful, particle contains seed + } + } + first = false; + // we have traced an inner hole or the wrong particle + if (!inside(x,y)) do { + x++; // traverse the hole + if (x>width) throw new RuntimeException("Wand Malfunction"); //should never happen + } while (!inside(x,y)); + do {x++;} while (inside(x,y)); //retry here; maybe no inner hole any more + } + } + + + /* Trace the outline, starting at a point (startX, startY). + * Pixel (startX-1, startY) must be outside, (startX, startY) must be inside, + * or reverse. Otherwise an endless loop will occur (and eat up all memory). + * Traces 8-connected inside pixels unless fourConnected is true. + * Returns whether the selection created encloses an 'inside' area + * and not an inner hole. + */ + private boolean traceEdge(int startX, int startY, boolean fourConnected) { + // Let us name the crossings between 4 pixels vertices, then the + // vertex (x,y) marked with '+', is between pixels (x-1, y-1) and (x,y): + // + // pixel x-1 x + // y-1 | + // ----+---- + // y | + // + // The four principal directions are numbered such that the direction + // number * 90 degrees gives the angle in the mathematical sense; and + // the directions to the adjacent pixels (for inside(x,y,direction) are + // at (number * 90 - 45) degrees: + // walking pixel + // directions: 1 directions: 2 | 1 + // 2 + 0 ----+---- + // 3 3 | 0 + // + // Directions, like angles, are cyclic; direction -1 = direction 3, etc. + // + // The algorithm: We walk along the border, from one vertex to the next, + // with the outside pixels always being at the left-hand side. + // For 8-connected tracing, we always trying to turn left as much as + // possible, to encompass an area as large as possible. + // Thus, when walking in direction 1 (up, -y), we start looking + // at the pixel in direction 2; if it is inside, we proceed in this + // direction (left); otherwise we try with direction 1 (up); if pixel 1 + // is not inside, we must proceed in direction 0 (right). + // + // 2 | 1 (i=inside, o=outside) + // direction 2 < ---+---- > direction 0 + // o | i + // ^ direction 1 = up = starting direction + // + // For 4-connected pixels, we try to go right as much as possible: + // First try with pixel 1; if it is outside we go in direction 0 (right). + // Otherwise, we examine pixel 2; if it is outside, we go in + // direction 1 (up); otherwise in direction 2 (left). + // + // When moving a closed loop, 'direction' gets incremented or decremented + // by a total of 360 degrees (i.e., 4) for counterclockwise and clockwise + // loops respectively. As the inside pixels are at the right side, we have + // got an outline of inner pixels after a cw loop (direction decremented + // by 4). + // + npoints = 0; + xmin = width; + final int startDirection; + if (inside(startX,startY)) // inside at left, outside right + startDirection = 1; // starting in direction 1 = up + else { + startDirection = 3; // starting in direction 3 = down + startY++; // continue after the boundary that has direction 3 + } + int x = startX; + int y = startY; + int direction = startDirection; + do { + int newDirection; + if (fourConnected) { + newDirection = direction; + do { + if (!inside(x, y, newDirection)) break; + newDirection++; + } while (newDirection < direction+2); + newDirection--; + } else { // 8-connected + newDirection = direction + 1; + do { + if (inside(x, y, newDirection)) break; + newDirection--; + } while (newDirection >= direction); + } + if (allPoints || newDirection!=direction) + addPoint(x,y); // a corner point of the outline polygon: add to list + switch (newDirection & 3) { // '& 3' is remainder modulo 4 + case 0: x++; break; + case 1: y--; break; + case 2: x--; break; + case 3: y++; break; + } + direction = newDirection; + } while (x!=startX || y!=startY || (direction&3)!=startDirection); + if (xpoints[0]!=x && !allPoints) // if the start point = end point is a corner: add to list + addPoint(x, y); + return (direction <= 0); // if we have done a clockwise loop, inside pixels are enclosed + } + + // add a point x,y to the outline polygon + private void addPoint (int x, int y) { + if (npoints==maxPoints) { + int[] xtemp = new int[maxPoints*2]; + int[] ytemp = new int[maxPoints*2]; + System.arraycopy(xpoints, 0, xtemp, 0, maxPoints); + System.arraycopy(ypoints, 0, ytemp, 0, maxPoints); + xpoints = xtemp; + ypoints = ytemp; + maxPoints *= 2; + } + xpoints[npoints] = x; + ypoints[npoints] = y; + npoints++; + if (xmin > x) xmin = x; + } + + // check pixel at (x,y), whether it is inside traced area + private boolean inside(int x, int y) { + if (x<0 || x>=width || y<0 || y>=height) + return false; + float value = getPixel(x, y); + return value>=lowerThreshold && value<=upperThreshold; + } + + // check pixel in a given direction from vertex (x,y) + private boolean inside(int x, int y, int direction) { + switch(direction & 3) { // '& 3' is remainder modulo 4 + case 0: return inside(x, y); + case 1: return inside(x, y-1); + case 2: return inside(x-1, y-1); + case 3: return inside(x-1, y); + } + return false; //will never occur, needed for the compiler + } + + // get a pixel value; returns Float.NaN if outside the field. + private float getPixel(int x, int y) { + if (x<0 || x>=width || y<0 || y>=height) + return Float.NaN; + if (bpixels!=null) + return bpixels[y*width + x] & 0xff; + else if (spixels!=null) + return spixels[y*width + x] & 0xffff; + else if (fpixels!=null) + return fpixels[y*width + x]; + else if (exactPixelValue) //RGB for exact match + return cpixels[y*width + x] & 0xffffff; //don't care for upper byte + else //gray value of RGB + return ip.getPixelValue(x,y); + } + + /* Are we tracing a one pixel wide line? Makes Legacy mode 8-connected instead of 4-connected */ + private boolean isLine(int xs, int ys) { + int r = 5; + int xmin=xs; + int xmax=xs+2*r; + if (xmax>=width) xmax=width-1; + int ymin=ys-r; + if (ymin<0) ymin=0; + int ymax=ys+r; + if (ymax>=height) ymax=height-1; + int area = 0; + int insideCount = 0; + for (int x=xmin; (x<=xmax); x++) + for (int y=ymin; y<=ymax; y++) { + area++; + if (inside(x,y)) + insideCount++; + } + if (IJ.debugMode) + IJ.log((((double)insideCount)/area<0.25?"line ":"blob ")+insideCount+" "+area+" "+IJ.d2s(((double)insideCount)/area)); + return ((double)insideCount)/area<0.25; + } + + /** Set 'true' and output will contain intermediate points for straight lines longer than one pixel. */ + public static void setAllPoints(boolean b) { + allPoints = b; + } + + /** Returns 'true' if output contains intermediate points for straight lines longer than one pixel. */ + public static boolean allPoints() { + return allPoints; + } + +} diff --git a/src/ij/gui/YesNoCancelDialog.java b/src/ij/gui/YesNoCancelDialog.java new file mode 100644 index 0000000..7f7e79b --- /dev/null +++ b/src/ij/gui/YesNoCancelDialog.java @@ -0,0 +1,138 @@ +package ij.gui; +import ij.*; +import java.awt.*; +import java.awt.event.*; + +/** A modal dialog box with a one line message and + "Yes", "No" and "Cancel" buttons. */ +public class YesNoCancelDialog extends Dialog implements ActionListener, KeyListener, WindowListener { + private Button yesB, noB, cancelB; + private boolean cancelPressed, yesPressed; + private boolean firstPaint = true; + + public YesNoCancelDialog(Frame parent, String title, String msg) { + this(parent, title, msg, " Yes ", " No "); + } + + public YesNoCancelDialog(Frame parent, String title, String msg, String yesLabel, String noLabel) { + super(parent, title, true); + setLayout(new BorderLayout()); + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 10, 10)); + MultiLineLabel message = new MultiLineLabel(msg); + message.setFont(new Font("Dialog", Font.PLAIN, 14)); + panel.add(message); + add("North", panel); + + panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.RIGHT, 15, 8)); + if (msg.startsWith("Save")) { + yesB = new Button(" Save "); + noB = new Button("Don't Save"); + cancelB = new Button(" Cancel "); + } else { + yesB = new Button(yesLabel); + noB = new Button(noLabel); + cancelB = new Button(" Cancel "); + } + yesB.addActionListener(this); + noB.addActionListener(this); + cancelB.addActionListener(this); + yesB.addKeyListener(this); + noB.addKeyListener(this); + cancelB.addKeyListener(this); + if (IJ.isWindows() || Prefs.dialogCancelButtonOnRight) { + panel.add(yesB); + panel.add(noB); + panel.add(cancelB); + } else { + panel.add(noB); + panel.add(cancelB); + panel.add(yesB); + } + if (IJ.isMacintosh()) + setResizable(false); + add("South", panel); + addWindowListener(this); + GUI.scale(this); + pack(); + yesB.requestFocusInWindow(); + GUI.centerOnImageJScreen(this); + show(); + } + + public void actionPerformed(ActionEvent e) { + if (e.getSource()==cancelB) + cancelPressed = true; + else if (e.getSource()==yesB) + yesPressed = true; + closeDialog(); + } + + /** Returns true if the user dismissed dialog by pressing "Cancel". */ + public boolean cancelPressed() { + return cancelPressed; + } + + /** Returns true if the user dismissed dialog by pressing "Yes". */ + public boolean yesPressed() { + return yesPressed; + } + + void closeDialog() { + dispose(); + } + + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyDown(keyCode); + if (keyCode==KeyEvent.VK_ENTER) { + if (cancelB.isFocusOwner()) { + cancelPressed = true; + closeDialog(); + } else if (noB.isFocusOwner()) { + closeDialog(); + } else { + yesPressed = true; + closeDialog(); + } + } else if (keyCode==KeyEvent.VK_Y||keyCode==KeyEvent.VK_S) { + yesPressed = true; + closeDialog(); + } else if (keyCode==KeyEvent.VK_N || keyCode==KeyEvent.VK_D) { + closeDialog(); + } else if (keyCode==KeyEvent.VK_ESCAPE||keyCode==KeyEvent.VK_C) { + cancelPressed = true; + closeDialog(); + IJ.resetEscape(); + } + } + + public void keyReleased(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyUp(keyCode); + } + + public void keyTyped(KeyEvent e) {} + + public void paint(Graphics g) { + super.paint(g); + if (firstPaint) { + yesB.requestFocus(); + firstPaint = false; + } + } + + public void windowClosing(WindowEvent e) { + cancelPressed = true; + closeDialog(); + } + + public void windowActivated(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + +} diff --git a/src/ij/io/BitBuffer.java b/src/ij/io/BitBuffer.java new file mode 100644 index 0000000..18832e3 --- /dev/null +++ b/src/ij/io/BitBuffer.java @@ -0,0 +1,65 @@ +package ij.io; + +/** + * A class for reading arbitrary numbers of bits from a byte array. + * @author Eric Kjellman egkjellman at wisc.edu + */ +public class BitBuffer { + + private int currentByte; + private int currentBit; + private byte[] byteBuffer; + private int eofByte; + private int[] backMask; + private int[] frontMask; + private boolean eofFlag; + + public BitBuffer(byte[] byteBuffer) { + this.byteBuffer = byteBuffer; + currentByte = 0; + currentBit = 0; + eofByte = byteBuffer.length; + backMask = new int[] {0x0000, 0x0001, 0x0003, 0x0007, + 0x000F, 0x001F, 0x003F, 0x007F}; + frontMask = new int[] {0x0000, 0x0080, 0x00C0, 0x00E0, + 0x00F0, 0x00F8, 0x00FC, 0x00FE}; + } + + public int getBits(int bitsToRead) { + if (bitsToRead == 0) + return 0; + if (eofFlag) + return -1; // Already at end of file + int toStore = 0; + while(bitsToRead != 0 && !eofFlag) { + if (bitsToRead >= 8 - currentBit) { + if (currentBit == 0) { // special + toStore = toStore << 8; + int cb = ((int) byteBuffer[currentByte]); + toStore += (cb<0 ? (int) 256 + cb : (int) cb); + bitsToRead -= 8; + currentByte++; + } else { + toStore = toStore << (8 - currentBit); + toStore += ((int) byteBuffer[currentByte]) & backMask[8 - currentBit]; + bitsToRead -= (8 - currentBit); + currentBit = 0; + currentByte++; + } + } else { + toStore = toStore << bitsToRead; + int cb = ((int) byteBuffer[currentByte]); + cb = (cb<0 ? (int) 256 + cb : (int) cb); + toStore += ((cb) & (0x00FF - frontMask[currentBit])) >> (8 - (currentBit + bitsToRead)); + currentBit += bitsToRead; + bitsToRead = 0; + } + if (currentByte == eofByte) { + eofFlag = true; + return toStore; + } + } + return toStore; + } + +} diff --git a/src/ij/io/DirectoryChooser.java b/src/ij/io/DirectoryChooser.java new file mode 100644 index 0000000..f2944ad --- /dev/null +++ b/src/ij/io/DirectoryChooser.java @@ -0,0 +1,137 @@ +package ij.io; +import ij.*; +import ij.gui.*; +import ij.plugin.frame.Recorder; +import ij.util.Java2; +import java.awt.*; +import java.io.*; +import javax.swing.*; +import javax.swing.filechooser.*; + +/** This class displays a dialog box that allows the user can select a directory. */ + public class DirectoryChooser { + private String directory; + private String title; + + /** Display a dialog using the specified title. */ + public DirectoryChooser(String title) { + this.title = title; + if (IJ.isMacOSX() && !Prefs.useJFileChooser) + getDirectoryUsingFileDialog(title); + else { + String macroOptions = Macro.getOptions(); + if (macroOptions!=null) + directory = Macro.getValue(macroOptions, title, null); + if (directory==null) { + if (EventQueue.isDispatchThread()) + getDirectoryUsingJFileChooserOnThisThread(title); + else + getDirectoryUsingJFileChooser(title); + } + } + } + + // runs JFileChooser on event dispatch thread to avoid possible thread deadlocks + void getDirectoryUsingJFileChooser(final String title) { + Java2.setSystemLookAndFeel(); + try { + EventQueue.invokeAndWait(new Runnable() { + public void run() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle(title); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setDragEnabled(true); + chooser.setTransferHandler(new DragAndDropHandler(chooser)); + String defaultDir = OpenDialog.getDefaultDirectory(); + if (defaultDir!=null) { + File f = new File(defaultDir); + if (IJ.debugMode) + IJ.log("DirectoryChooser,setSelectedFileW: "+f); + chooser.setSelectedFile(f); + } + chooser.setApproveButtonText("Select"); + if (chooser.showOpenDialog(null)==JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + directory = file.getAbsolutePath(); + directory = IJ.addSeparator(directory); + OpenDialog.setDefaultDirectory(directory); + } + } + }); + } catch (Exception e) {} + } + + // Choose a directory using JFileChooser on the current thread + void getDirectoryUsingJFileChooserOnThisThread(final String title) { + Java2.setSystemLookAndFeel(); + try { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle(title); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setDragEnabled(true); + chooser.setTransferHandler(new DragAndDropHandler(chooser)); + String defaultDir = OpenDialog.getDefaultDirectory(); + if (defaultDir!=null) { + File f = new File(defaultDir); + if (IJ.debugMode) + IJ.log("DirectoryChooser,setSelectedFile: "+f); + chooser.setSelectedFile(f); + } + chooser.setApproveButtonText("Select"); + if (chooser.showOpenDialog(null)==JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + directory = file.getAbsolutePath(); + directory = IJ.addSeparator(directory); + OpenDialog.setDefaultDirectory(directory); + } + } catch (Exception e) {} + } + + // On Mac OS X, we can select directories using the native file open dialog + void getDirectoryUsingFileDialog(String title) { + boolean saveUseJFC = Prefs.useJFileChooser; + Prefs.useJFileChooser = false; + System.setProperty("apple.awt.fileDialogForDirectories", "true"); + String dir=null, name=null; + String defaultDir = OpenDialog.getDefaultDirectory(); + if (defaultDir!=null) { + File f = new File(defaultDir); + dir = f.getParent(); + name = f.getName(); + } + if (IJ.debugMode) + IJ.log("DirectoryChooser: dir=\""+dir+"\", file=\""+name+"\""); + OpenDialog od = new OpenDialog(title, dir, null); + String odDir = od.getDirectory(); + if (odDir==null) + directory = null; + else { + directory = odDir + od.getFileName() + "/"; + OpenDialog.setDefaultDirectory(directory); + } + System.setProperty("apple.awt.fileDialogForDirectories", "false"); + Prefs.useJFileChooser = saveUseJFC; + } + + /** Returns the directory selected by the user. */ + public String getDirectory() { + if (IJ.debugMode) + IJ.log("DirectoryChooser.getDirectory: "+directory); + if (Recorder.record && !IJ.isMacOSX()) + Recorder.recordPath(title, directory); + return directory; + } + + /** Sets the default directory presented in the dialog. */ + public static void setDefaultDirectory(String dir) { + if (dir==null || (new File(dir)).isDirectory()) + OpenDialog.setDefaultDirectory(dir); + } + + //private void setSystemLookAndFeel() { + // try { + // UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + // } catch(Throwable t) {} + //} + +} diff --git a/src/ij/io/DragAndDropHandler.java b/src/ij/io/DragAndDropHandler.java new file mode 100644 index 0000000..d14227f --- /dev/null +++ b/src/ij/io/DragAndDropHandler.java @@ -0,0 +1,95 @@ +package ij.io; +import ij.*; +import ij.gui.*; +import ij.plugin.frame.Recorder; +import ij.util.Java2; +import java.awt.*; +import java.io.*; +import java.util.ArrayList; //no need to import java.util.List; it would be ambiguous because of java.awt.List +import java.net.URI; +import java.net.URISyntaxException; +import javax.swing.*; +import javax.swing.filechooser.*; +import java.awt.datatransfer.*; + +/** This class handles drag&drop onto JFileChoosers. */ + public class DragAndDropHandler extends TransferHandler { + private JFileChooser jFileChooser; + + /** Given a JFileChooser 'fc', this is how to use this class: + *

+	 *     fc.setDragEnabled(true);
+	 *     fc.setTransferHandler(new DragAndDropHandler(fc));
+	 * 
+ */ + public DragAndDropHandler(JFileChooser jFileChooser) { + super(); + this.jFileChooser = jFileChooser; + } + + /** Returns whether any of the transfer flavors is supported */ + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + for (DataFlavor dataFlavor : transferFlavors) { + if (isSupportedTransferFlavor(dataFlavor)) + return true; + } + return false; + } + + /** Imports the drag&drop file or list of files and sets the JFileChooser to this. + * Returns true if successful */ + public boolean importData(JComponent comp, Transferable t) { + DataFlavor[] transferFlavors = t.getTransferDataFlavors(); + for (DataFlavor dataFlavor : transferFlavors) { + try { + java.util.List fileList = null; + if (dataFlavor.isFlavorJavaFileListType()) { + fileList = (java.util.List)t.getTransferData(DataFlavor.javaFileListFlavor); + if (IJ.debugMode) IJ.log("dragAndDrop FileList size="+fileList.size()+" first: "+fileList.get(0)); + } else if (isSupportedTransferFlavor(dataFlavor)) { + String str = (String)t.getTransferData(dataFlavor); + if (IJ.debugMode) IJ.log("dragAndDrop str="+str); + String[] strs = str.split("[\n\r]+"); //multiple files are separate lines + fileList = new ArrayList(strs.length); + for (String s : strs) { + if (s.length() > 0) { + File file = new File(s); //Try whether it is a plain path (pointing to an existing file) + if (!file.exists()) try { + file = new File(new URI(s)); //When not successful, try a whether it is a URL, e.g. file:///absolute/path/to/file + } catch (URISyntaxException e) {continue;} + if (file.exists()) //We only accept drag&drop of existing files + fileList.add(file); + } + } + } + if (fileList == null || fileList.size() ==0) continue; //this data flavor did not work + + File firstFile = fileList.get(0); + if (jFileChooser.isMultiSelectionEnabled()) { //multiple files accepted + File dir = firstFile.getParentFile(); + jFileChooser.setCurrentDirectory(dir); + File[] files = fileList.toArray(new File[0]); + jFileChooser.setSelectedFiles(files); + } else { + File file = firstFile; //single file required; if we get more take the first one + if (jFileChooser.getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY && !file.isDirectory()) + file = file.getParentFile(); //if a directory is required and we get a file, use its directory + if (jFileChooser.getDialogType() == JFileChooser.SAVE_DIALOG && file.isDirectory()) + jFileChooser.setCurrentDirectory(file); //in a save operation, if we get a directory, move there + else + jFileChooser.setSelectedFile(file); + } + jFileChooser.rescanCurrentDirectory(); + return true; + } catch (Exception e) {if (IJ.debugMode) IJ.handleException(e);} + } + return false; + } + + /** Returns whether this transfer flavor is supported. We support File Lists and Strings (plain or as list of URLs). */ + public boolean isSupportedTransferFlavor(DataFlavor flavor) { + return flavor.isFlavorJavaFileListType() || + (flavor.getRepresentationClass() == String.class && + (flavor.getMimeType().startsWith("text/uri-list") || flavor.getMimeType().startsWith("text/plain"))); + } +} diff --git a/src/ij/io/FileInfo.java b/src/ij/io/FileInfo.java new file mode 100644 index 0000000..f2f303d --- /dev/null +++ b/src/ij/io/FileInfo.java @@ -0,0 +1,270 @@ +package ij.io; +import ij.VirtualStack; +import ij.IJ; +import java.io.*; +import java.util.Properties; + +/** This class consists of public fields that describe an image file. */ +public class FileInfo implements Cloneable { + + /** 8-bit unsigned integer (0-255). */ + public static final int GRAY8 = 0; + + /** 16-bit signed integer (-32768-32767). Imported signed images + are converted to unsigned by adding 32768. */ + public static final int GRAY16_SIGNED = 1; + + /** 16-bit unsigned integer (0-65535). */ + public static final int GRAY16_UNSIGNED = 2; + + /** 32-bit signed integer. Imported 32-bit integer images are + converted to floating-point. */ + public static final int GRAY32_INT = 3; + + /** 32-bit floating-point. */ + public static final int GRAY32_FLOAT = 4; + + /** 8-bit unsigned integer with color lookup table. */ + public static final int COLOR8 = 5; + + /** 24-bit interleaved RGB. Import/export only. */ + public static final int RGB = 6; + + /** 24-bit planer RGB. Import only. */ + public static final int RGB_PLANAR = 7; + + /** 1-bit black and white. Import only. */ + public static final int BITMAP = 8; + + /** 32-bit interleaved ARGB. Import only. */ + public static final int ARGB = 9; + + /** 24-bit interleaved BGR. Import only. */ + public static final int BGR = 10; + + /** 32-bit unsigned integer. Imported 32-bit integer images are + converted to floating-point. */ + public static final int GRAY32_UNSIGNED = 11; + + /** 48-bit interleaved RGB. */ + public static final int RGB48 = 12; + + /** 12-bit unsigned integer (0-4095). Import only. */ + public static final int GRAY12_UNSIGNED = 13; + + /** 24-bit unsigned integer. Import only. */ + public static final int GRAY24_UNSIGNED = 14; + + /** 32-bit interleaved BARG (MCID). Import only. */ + public static final int BARG = 15; + + /** 64-bit floating-point. Import only.*/ + public static final int GRAY64_FLOAT = 16; + + /** 48-bit planar RGB. Import only. */ + public static final int RGB48_PLANAR = 17; + + /** 32-bit interleaved ABGR. Import only. */ + public static final int ABGR = 18; + + /** 32-bit interleaved CMYK. Import only. */ + public static final int CMYK = 19; + + // File formats + public static final int UNKNOWN = 0; + public static final int RAW = 1; + public static final int TIFF = 2; + public static final int GIF_OR_JPG = 3; + public static final int FITS = 4; + public static final int BMP = 5; + public static final int DICOM = 6; + public static final int ZIP_ARCHIVE = 7; + public static final int PGM = 8; + public static final int IMAGEIO = 9; + + // Compression modes + public static final int COMPRESSION_UNKNOWN = 0; + public static final int COMPRESSION_NONE= 1; + public static final int LZW = 2; + public static final int LZW_WITH_DIFFERENCING = 3; + public static final int JPEG = 4; + public static final int PACK_BITS = 5; + public static final int ZIP = 6; + + /* File format (TIFF, GIF_OR_JPG, BMP, etc.). Used by the File/Revert command */ + public int fileFormat; + + /* File type (GRAY8, GRAY_16_UNSIGNED, RGB, etc.) */ + public int fileType; + public String fileName; + public String directory; + public String url; + public int width; + public int height; + public int offset=0; // Use getOffset() to read + public int nImages; + public int gapBetweenImages; // Use getGap() to read + public boolean whiteIsZero; + public boolean intelByteOrder; + public int compression; + public int[] stripOffsets; + public int[] stripLengths; + public int rowsPerStrip; + public int lutSize; + public byte[] reds; + public byte[] greens; + public byte[] blues; + public Object pixels; + public String debugInfo; + public String[] sliceLabels; + public String info; + public InputStream inputStream; + public VirtualStack virtualStack; + public int sliceNumber; // used by FileInfoVirtualStack + + public double pixelWidth=1.0; + public double pixelHeight=1.0; + public double pixelDepth=1.0; + public String unit; + public int calibrationFunction; + public double[] coefficients; + public String valueUnit; + public double frameInterval; + public String description; + // Use longOffset instead of offset when offset>2147483647. + public long longOffset; // Use getOffset() to read + // Use longGap instead of gapBetweenImages when gap>2147483647. + public long longGap; // Use getGap() to read + // Extra metadata to be stored in the TIFF header + public int[] metaDataTypes; // must be < 0xffffff + public byte[][] metaData; + public double[] displayRanges; + public byte[][] channelLuts; + public byte[] plot; // serialized plot + public byte[] roi; // serialized roi + public byte[][] overlay; // serialized overlay objects + public int samplesPerPixel; + public String openNextDir, openNextName; + public String[] properties; // {key,value,key,value,...} + public boolean imageSaved; + + /** Creates a FileInfo object with all of its fields set to their default value. */ + public FileInfo() { + // assign default values + fileFormat = UNKNOWN; + fileType = GRAY8; + fileName = "Untitled"; + directory = ""; + url = ""; + nImages = 1; + compression = COMPRESSION_NONE; + samplesPerPixel = 1; + } + + /** Returns the file path. */ + public String getFilePath() { + String dir = directory; + if (dir==null) + dir = ""; + dir = IJ.addSeparator(dir); + return dir + fileName; + } + + /** Returns the offset as a long. */ + public final long getOffset() { + return longOffset>0L?longOffset:((long)offset)&0xffffffffL; + } + + /** Returns the gap between images as a long. */ + public final long getGap() { + return longGap>0L?longGap:((long)gapBetweenImages)&0xffffffffL; + } + + /** Returns the number of bytes used per pixel. */ + public int getBytesPerPixel() { + switch (fileType) { + case GRAY8: case COLOR8: case BITMAP: return 1; + case GRAY16_SIGNED: case GRAY16_UNSIGNED: return 2; + case GRAY32_INT: case GRAY32_UNSIGNED: case GRAY32_FLOAT: case ARGB: case GRAY24_UNSIGNED: case BARG: case ABGR: case CMYK: return 4; + case RGB: case RGB_PLANAR: case BGR: return 3; + case RGB48: case RGB48_PLANAR: return 6; + case GRAY64_FLOAT : return 8; + default: return 0; + } + } + + public String toString() { + return + "name=" + fileName + + ", dir=" + directory + + ", width=" + width + + ", height=" + height + + ", nImages=" + nImages + + ", offset=" + getOffset() + + ", gap=" + getGap() + + ", type=" + getType() + + ", byteOrder=" + (intelByteOrder?"little":"big") + + ", format=" + fileFormat + + ", url=" + url + + ", whiteIsZero=" + (whiteIsZero?"t":"f") + + ", lutSize=" + lutSize + + ", comp=" + compression + + ", ranges=" + (displayRanges!=null?""+displayRanges.length/2:"null") + + ", samples=" + samplesPerPixel; + } + + /** Returns JavaScript code that can be used to recreate this FileInfo. */ + public String getCode() { + String code = "fi = new FileInfo();\n"; + String type = null; + if (fileType==GRAY8) + type = "GRAY8"; + else if (fileType==GRAY16_UNSIGNED) + type = "GRAY16_UNSIGNED"; + else if (fileType==GRAY32_FLOAT) + type = "GRAY32_FLOAT"; + else if (fileType==RGB) + type = "RGB"; + if (type!=null) + code += "fi.fileType = FileInfo."+type+";\n"; + code += "fi.width = "+width+";\n"; + code += "fi.height = "+height+";\n"; + if (nImages>1) + code += "fi.nImages = "+nImages+";\n"; + if (getOffset()>0) + code += "fi.longOffset = "+getOffset()+";\n"; + if (intelByteOrder) + code += "fi.intelByteOrder = true;\n"; + return code; + } + + private String getType() { + switch (fileType) { + case GRAY8: return "byte"; + case GRAY16_SIGNED: return "short"; + case GRAY16_UNSIGNED: return "ushort"; + case GRAY32_INT: return "int"; + case GRAY32_UNSIGNED: return "uint"; + case GRAY32_FLOAT: return "float"; + case COLOR8: return "byte(lut)"; + case RGB: return "RGB"; + case RGB_PLANAR: return "RGB(p)"; + case RGB48: return "RGB48"; + case BITMAP: return "bitmap"; + case ARGB: return "ARGB"; + case ABGR: return "ABGR"; + case BGR: return "BGR"; + case BARG: return "BARG"; + case CMYK: return "CMYK"; + case GRAY64_FLOAT: return "double"; + case RGB48_PLANAR: return "RGB48(p)"; + default: return ""; + } + } + + public synchronized Object clone() { + try {return super.clone();} + catch (CloneNotSupportedException e) {return null;} + } + +} diff --git a/src/ij/io/FileOpener.java b/src/ij/io/FileOpener.java new file mode 100644 index 0000000..202121a --- /dev/null +++ b/src/ij/io/FileOpener.java @@ -0,0 +1,676 @@ +package ij.io; +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.net.*; +import java.util.*; +import java.util.zip.GZIPInputStream; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import ij.*; +import ij.plugin.frame.*; + +/** + * Opens or reverts an image specified by a FileInfo object. Images can + * be loaded from either a file (directory+fileName) or a URL (url+fileName). + * Here is an example: + *
+ *   public class FileInfo_Test implements PlugIn {
+ *     public void run(String arg) {
+ *       FileInfo fi = new FileInfo();
+ *       fi.width = 256;
+ *       fi.height = 254;
+ *       fi.offset = 768;
+ *       fi.fileName = "blobs.tif";
+ *       fi.directory = "/Users/wayne/Desktop/";
+ *       new FileOpener(fi).open();
+ *     }  
+ *   }	
+ * 
+ */ +public class FileOpener { + + private FileInfo fi; + private int width, height; + private static boolean showConflictMessage = true; + private double minValue, maxValue; + private static boolean silentMode; + + public FileOpener(FileInfo fi) { + this.fi = fi; + if (fi!=null) { + width = fi.width; + height = fi.height; + } + if (IJ.debugMode) IJ.log("FileInfo: "+fi); + } + + /** Opens the image and returns it has an ImagePlus object. */ + public ImagePlus openImage() { + boolean wasRecording = Recorder.record; + Recorder.record = false; + ImagePlus imp = open(false); + Recorder.record = wasRecording; + return imp; + } + + /** Opens the image and displays it. */ + public void open() { + open(true); + } + + /** Obsolete, replaced by openImage() and open(). */ + public ImagePlus open(boolean show) { + + ImagePlus imp=null; + Object pixels; + ProgressBar pb=null; + ImageProcessor ip; + + ColorModel cm = createColorModel(fi); + if (fi.nImages>1) + return openStack(cm, show); + switch (fi.fileType) { + case FileInfo.GRAY8: + case FileInfo.COLOR8: + case FileInfo.BITMAP: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ByteProcessor(width, height, (byte[])pixels, cm); + imp = new ImagePlus(fi.fileName, ip); + break; + case FileInfo.GRAY16_SIGNED: + case FileInfo.GRAY16_UNSIGNED: + case FileInfo.GRAY12_UNSIGNED: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ShortProcessor(width, height, (short[])pixels, cm); + imp = new ImagePlus(fi.fileName, ip); + break; + case FileInfo.GRAY32_INT: + case FileInfo.GRAY32_UNSIGNED: + case FileInfo.GRAY32_FLOAT: + case FileInfo.GRAY24_UNSIGNED: + case FileInfo.GRAY64_FLOAT: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new FloatProcessor(width, height, (float[])pixels, cm); + imp = new ImagePlus(fi.fileName, ip); + break; + case FileInfo.RGB: + case FileInfo.BGR: + case FileInfo.ARGB: + case FileInfo.ABGR: + case FileInfo.BARG: + case FileInfo.RGB_PLANAR: + case FileInfo.CMYK: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ColorProcessor(width, height, (int[])pixels); + if (fi.fileType==FileInfo.CMYK) + ip.invert(); + imp = new ImagePlus(fi.fileName, ip); + break; + case FileInfo.RGB48: + case FileInfo.RGB48_PLANAR: + boolean planar = fi.fileType==FileInfo.RGB48_PLANAR; + Object[] pixelArray = (Object[])readPixels(fi); + if (pixelArray==null) return null; + int nChannels = 3; + ImageStack stack = new ImageStack(width, height); + stack.addSlice("Red", pixelArray[0]); + stack.addSlice("Green", pixelArray[1]); + stack.addSlice("Blue", pixelArray[2]); + if (fi.samplesPerPixel==4 && pixelArray.length==4) { + stack.addSlice("Gray", pixelArray[3]); + nChannels = 4; + } + imp = new ImagePlus(fi.fileName, stack); + imp.setDimensions(nChannels, 1, 1); + if (planar) + imp.getProcessor().resetMinAndMax(); + imp.setFileInfo(fi); + int mode = IJ.COMPOSITE; + if (fi.description!=null) { + if (fi.description.indexOf("mode=color")!=-1) + mode = IJ.COLOR; + else if (fi.description.indexOf("mode=gray")!=-1) + mode = IJ.GRAYSCALE; + } + imp = new CompositeImage(imp, mode); + if (!planar && fi.displayRanges==null) { + if (nChannels==4) + ((CompositeImage)imp).resetDisplayRanges(); + else { + for (int c=1; c<=3; c++) { + imp.setPosition(c, 1, 1); + imp.setDisplayRange(minValue, maxValue); + } + imp.setPosition(1, 1, 1); + } + } + if (fi.whiteIsZero) // cmyk? + IJ.run(imp, "Invert", ""); + break; + } + imp.setFileInfo(fi); + setCalibration(imp); + if (fi.info!=null) + imp.setProperty("Info", fi.info); + if (fi.sliceLabels!=null&&fi.sliceLabels.length==1&&fi.sliceLabels[0]!=null) + imp.setProp("Slice_Label", fi.sliceLabels[0]); + if (fi.plot!=null) try { + Plot plot = new Plot(imp, new ByteArrayInputStream(fi.plot)); + imp.setProperty(Plot.PROPERTY_KEY, plot); + } catch (Exception e) { IJ.handleException(e); } + if (fi.roi!=null) + decodeAndSetRoi(imp, fi); + if (fi.overlay!=null) + setOverlay(imp, fi.overlay); + if (fi.properties!=null) + imp.setProperties(fi.properties); + if (show) imp.show(); + return imp; + } + + public ImageProcessor openProcessor() { + Object pixels; + ProgressBar pb=null; + ImageProcessor ip = null; + ColorModel cm = createColorModel(fi); + switch (fi.fileType) { + case FileInfo.GRAY8: + case FileInfo.COLOR8: + case FileInfo.BITMAP: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ByteProcessor(width, height, (byte[])pixels, cm); + break; + case FileInfo.GRAY16_SIGNED: + case FileInfo.GRAY16_UNSIGNED: + case FileInfo.GRAY12_UNSIGNED: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ShortProcessor(width, height, (short[])pixels, cm); + break; + case FileInfo.GRAY32_INT: + case FileInfo.GRAY32_UNSIGNED: + case FileInfo.GRAY32_FLOAT: + case FileInfo.GRAY24_UNSIGNED: + case FileInfo.GRAY64_FLOAT: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new FloatProcessor(width, height, (float[])pixels, cm); + break; + case FileInfo.RGB: + case FileInfo.BGR: + case FileInfo.ARGB: + case FileInfo.ABGR: + case FileInfo.BARG: + case FileInfo.RGB_PLANAR: + case FileInfo.CMYK: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ColorProcessor(width, height, (int[])pixels); + if (fi.fileType==FileInfo.CMYK) + ip.invert(); + break; + } + return ip; + } + + void setOverlay(ImagePlus imp, byte[][] rois) { + Overlay overlay = new Overlay(); + Overlay proto = null; + for (int i=0; i1) + IJ.setTool("multi-point"); + } + + void setStackDisplayRange(ImagePlus imp) { + ImageStack stack = imp.getStack(); + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + int n = stack.size(); + for (int i=1; i<=n; i++) { + if (!silentMode) + IJ.showStatus("Calculating stack min and max: "+i+"/"+n); + ImageProcessor ip = stack.getProcessor(i); + ip.resetMinAndMax(); + if (ip.getMin()max) + max = ip.getMax(); + } + imp.getProcessor().setMinAndMax(min, max); + imp.updateAndDraw(); + } + + /** Restores the original version of the specified image. */ + public void revertToSaved(ImagePlus imp) { + if (fi==null) + return; + String path = fi.getFilePath(); + if (fi.url!=null && !fi.url.equals("") && (fi.directory==null||fi.directory.equals(""))) + path = fi.url; + IJ.showStatus("Loading: " + path); + ImagePlus imp2 = null; + if (!path.endsWith(".raw")) + imp2 = IJ.openImage(path); + if (imp2!=null) + imp.setImage(imp2); + else { + if (fi.nImages>1) + return; + Object pixels = readPixels(fi); + if (pixels==null) return; + ColorModel cm = createColorModel(fi); + ImageProcessor ip = null; + switch (fi.fileType) { + case FileInfo.GRAY8: + case FileInfo.COLOR8: + case FileInfo.BITMAP: + ip = new ByteProcessor(width, height, (byte[])pixels, cm); + imp.setProcessor(null, ip); + break; + case FileInfo.GRAY16_SIGNED: + case FileInfo.GRAY16_UNSIGNED: + case FileInfo.GRAY12_UNSIGNED: + ip = new ShortProcessor(width, height, (short[])pixels, cm); + imp.setProcessor(null, ip); + break; + case FileInfo.GRAY32_INT: + case FileInfo.GRAY32_FLOAT: + ip = new FloatProcessor(width, height, (float[])pixels, cm); + imp.setProcessor(null, ip); + break; + case FileInfo.RGB: + case FileInfo.BGR: + case FileInfo.ARGB: + case FileInfo.ABGR: + case FileInfo.RGB_PLANAR: + Image img = Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(width, height, (int[])pixels, 0, width)); + imp.setImage(img); + break; + case FileInfo.CMYK: + ip = new ColorProcessor(width, height, (int[])pixels); + ip.invert(); + imp.setProcessor(null, ip); + break; + } + } + } + + void setCalibration(ImagePlus imp) { + if (fi.fileType==FileInfo.GRAY16_SIGNED) { + if (IJ.debugMode) IJ.log("16-bit signed"); + imp.getLocalCalibration().setSigned16BitCalibration(); + } + Properties props = decodeDescriptionString(fi); + Calibration cal = imp.getCalibration(); + boolean calibrated = false; + if (fi.pixelWidth>0.0 && fi.unit!=null) { + if (Prefs.convertToMicrons && fi.pixelWidth<=0.0001 && fi.unit.equals("cm")) { + fi.pixelWidth *= 10000.0; + fi.pixelHeight *= 10000.0; + if (fi.pixelDepth!=1.0) + fi.pixelDepth *= 10000.0; + fi.unit = "um"; + } + cal.pixelWidth = fi.pixelWidth; + cal.pixelHeight = fi.pixelHeight; + cal.pixelDepth = fi.pixelDepth; + cal.setUnit(fi.unit); + calibrated = true; + } + + if (fi.valueUnit!=null) { + if (imp.getBitDepth()==32) + cal.setValueUnit(fi.valueUnit); + else { + int f = fi.calibrationFunction; + if ((f>=Calibration.STRAIGHT_LINE && f<=Calibration.EXP_RECOVERY && fi.coefficients!=null) + || f==Calibration.UNCALIBRATED_OD) { + boolean zeroClip = props!=null && props.getProperty("zeroclip", "false").equals("true"); + cal.setFunction(f, fi.coefficients, fi.valueUnit, zeroClip); + calibrated = true; + } + } + } + + if (calibrated) + checkForCalibrationConflict(imp, cal); + + if (fi.frameInterval!=0.0) + cal.frameInterval = fi.frameInterval; + + if (props==null) + return; + + cal.xOrigin = getDouble(props,"xorigin"); + cal.yOrigin = getDouble(props,"yorigin"); + cal.zOrigin = getDouble(props,"zorigin"); + cal.setInvertY(getBoolean(props, "inverty")); + cal.info = props.getProperty("info"); + + cal.fps = getDouble(props,"fps"); + cal.loop = getBoolean(props, "loop"); + cal.frameInterval = getDouble(props,"finterval"); + cal.setTimeUnit(props.getProperty("tunit", "sec")); + cal.setYUnit(props.getProperty("yunit")); + cal.setZUnit(props.getProperty("zunit")); + + double displayMin = getDouble(props,"min"); + double displayMax = getDouble(props,"max"); + if (!(displayMin==0.0&&displayMax==0.0)) { + int type = imp.getType(); + ImageProcessor ip = imp.getProcessor(); + if (type==ImagePlus.GRAY8 || type==ImagePlus.COLOR_256) + ip.setMinAndMax(displayMin, displayMax); + else if (type==ImagePlus.GRAY16 || type==ImagePlus.GRAY32) { + if (ip.getMin()!=displayMin || ip.getMax()!=displayMax) + ip.setMinAndMax(displayMin, displayMax); + } + } + + if (getBoolean(props, "8bitcolor")) + imp.setTypeToColor256(); // set type to COLOR_256 + + int stackSize = imp.getStackSize(); + if (stackSize>1) { + int channels = (int)getDouble(props,"channels"); + int slices = (int)getDouble(props,"slices"); + int frames = (int)getDouble(props,"frames"); + if (channels==0) channels = 1; + if (slices==0) slices = 1; + if (frames==0) frames = 1; + //IJ.log("setCalibration: "+channels+" "+slices+" "+frames); + if (channels*slices*frames==stackSize) { + imp.setDimensions(channels, slices, frames); + if (getBoolean(props, "hyperstack")) + imp.setOpenAsHyperStack(true); + } + } + } + + + void checkForCalibrationConflict(ImagePlus imp, Calibration cal) { + Calibration gcal = imp.getGlobalCalibration(); + if (gcal==null || !showConflictMessage || IJ.isMacro()) + return; + if (cal.pixelWidth==gcal.pixelWidth && cal.getUnit().equals(gcal.getUnit())) + return; + GenericDialog gd = new GenericDialog(imp.getTitle()); + gd.addMessage("The calibration of this image conflicts\nwith the current global calibration."); + gd.addCheckbox("Disable_Global Calibration", true); + gd.addCheckbox("Disable_these Messages", false); + gd.showDialog(); + if (gd.wasCanceled()) return; + boolean disable = gd.getNextBoolean(); + if (disable) { + imp.setGlobalCalibration(null); + imp.setCalibration(cal); + WindowManager.repaintImageWindows(); + } + boolean dontShow = gd.getNextBoolean(); + if (dontShow) showConflictMessage = false; + } + + /** Returns an IndexColorModel for the image specified by this FileInfo. */ + public ColorModel createColorModel(FileInfo fi) { + if (fi.lutSize>0) + return new IndexColorModel(8, fi.lutSize, fi.reds, fi.greens, fi.blues); + else + return LookUpTable.createGrayscaleColorModel(fi.whiteIsZero); + } + + /** Returns an InputStream for the image described by this FileInfo. */ + public InputStream createInputStream(FileInfo fi) throws IOException, MalformedURLException { + InputStream is = null; + boolean gzip = fi.fileName!=null && (fi.fileName.endsWith(".gz")||fi.fileName.endsWith(".GZ")); + if (fi.inputStream!=null) + is = fi.inputStream; + else if (fi.url!=null && !fi.url.equals("")) + is = new URL(fi.url+fi.fileName).openStream(); + else { + if (fi.directory!=null && fi.directory.length()>0 && !(fi.directory.endsWith(Prefs.separator)||fi.directory.endsWith("/"))) + fi.directory += Prefs.separator; + File f = new File(fi.getFilePath()); + if (gzip) fi.compression = FileInfo.COMPRESSION_UNKNOWN; + if (f==null || !f.exists() || f.isDirectory() || !validateFileInfo(f, fi)) + is = null; + else + is = new FileInputStream(f); + } + if (is!=null) { + if (fi.compression>=FileInfo.LZW) + is = new RandomAccessStream(is); + else if (gzip) + is = new GZIPInputStream(is, 50000); + } + return is; + } + + static boolean validateFileInfo(File f, FileInfo fi) { + long offset = fi.getOffset(); + long length = 0; + if (fi.width<=0 || fi.height<=0) { + error("Width or height <= 0.", fi, offset, length); + return false; + } + if (offset>=0 && offset<1000L) + return true; + if (offset<0L) { + error("Offset is negative.", fi, offset, length); + return false; + } + if (fi.fileType==FileInfo.BITMAP || fi.compression!=FileInfo.COMPRESSION_NONE) + return true; + length = f.length(); + long size = fi.width*fi.height*fi.getBytesPerPixel(); + size = fi.nImages>1?size:size/4; + if (fi.height==1) size = 0; // allows plugins to read info of unknown length at end of file + if (offset+size>length) { + error("Offset + image size > file length.", fi, offset, length); + return false; + } + return true; + } + + static void error(String msg, FileInfo fi, long offset, long length) { + String msg2 = "FileInfo parameter error. \n" + +msg + "\n \n" + +" Width: " + fi.width + "\n" + +" Height: " + fi.height + "\n" + +" Offset: " + offset + "\n" + +" Bytes/pixel: " + fi.getBytesPerPixel() + "\n" + +(length>0?" File length: " + length + "\n":""); + if (silentMode) { + IJ.log("Error opening "+fi.getFilePath()); + IJ.log(msg2); + } else + IJ.error("FileOpener", msg2); + } + + + /** Reads the pixel data from an image described by a FileInfo object. */ + Object readPixels(FileInfo fi) { + Object pixels = null; + try { + InputStream is = createInputStream(fi); + if (is==null) + return null; + ImageReader reader = new ImageReader(fi); + pixels = reader.readPixels(is); + minValue = reader.min; + maxValue = reader.max; + is.close(); + } + catch (Exception e) { + if (!Macro.MACRO_CANCELED.equals(e.getMessage())) + IJ.handleException(e); + } + return pixels; + } + + public Properties decodeDescriptionString(FileInfo fi) { + if (fi.description==null || fi.description.length()<7) + return null; + if (IJ.debugMode) + IJ.log("Image Description: " + new String(fi.description).replace('\n',' ')); + if (!fi.description.startsWith("ImageJ")) + return null; + Properties props = new Properties(); + InputStream is = new ByteArrayInputStream(fi.description.getBytes()); + try {props.load(is); is.close();} + catch (IOException e) {return null;} + String dsUnit = props.getProperty("unit",""); + if ("cm".equals(fi.unit) && "um".equals(dsUnit)) { + fi.pixelWidth *= 10000; + fi.pixelHeight *= 10000; + } + fi.unit = dsUnit; + Double n = getNumber(props,"cf"); + if (n!=null) fi.calibrationFunction = n.intValue(); + double c[] = new double[5]; + int count = 0; + for (int i=0; i<5; i++) { + n = getNumber(props,"c"+i); + if (n==null) break; + c[i] = n.doubleValue(); + count++; + } + if (count>=2) { + fi.coefficients = new double[count]; + for (int i=0; i1.0) + fi.nImages = (int)n.doubleValue(); + n = getNumber(props, "spacing"); + if (n!=null) { + double spacing = n.doubleValue(); + if (spacing<0) spacing = -spacing; + fi.pixelDepth = spacing; + } + String name = props.getProperty("name"); + if (name!=null) + fi.fileName = name; + return props; + } + + private Double getNumber(Properties props, String key) { + String s = props.getProperty(key); + if (s!=null) { + try { + return Double.valueOf(s); + } catch (NumberFormatException e) {} + } + return null; + } + + private double getDouble(Properties props, String key) { + Double n = getNumber(props, key); + return n!=null?n.doubleValue():0.0; + } + + private boolean getBoolean(Properties props, String key) { + String s = props.getProperty(key); + return s!=null&&s.equals("true")?true:false; + } + + public static void setShowConflictMessage(boolean b) { + showConflictMessage = b; + } + + static void setSilentMode(boolean mode) { + silentMode = mode; + } + + +} diff --git a/src/ij/io/FileSaver.java b/src/ij/io/FileSaver.java new file mode 100644 index 0000000..a7ef326 --- /dev/null +++ b/src/ij/io/FileSaver.java @@ -0,0 +1,837 @@ +package ij.io; +import java.awt.*; +import java.io.*; +import java.util.zip.*; +import ij.*; +import ij.process.*; +import ij.measure.Calibration; +import ij.plugin.filter.Analyzer; +import ij.plugin.frame.Recorder; +import ij.plugin.JpegWriter; +import ij.plugin.Orthogonal_Views; +import ij.gui.*; +import ij.measure.Measurements; +import ij.util.Tools; +import javax.imageio.*; + +/** Saves images in tiff, gif, jpeg, raw, zip and text format. */ +public class FileSaver { + + public static final int DEFAULT_JPEG_QUALITY = 85; + private static int jpegQuality; + private static int bsize = 32768; // 32K default buffer size + + static {setJpegQuality(ij.Prefs.getInt(ij.Prefs.JPEG, DEFAULT_JPEG_QUALITY));} + + private static String defaultDirectory = null; + private ImagePlus imp; + private FileInfo fi; + private String name; + private String directory; + private boolean saveName; + + /** Constructs a FileSaver from an ImagePlus. */ + public FileSaver(ImagePlus imp) { + this.imp = imp; + fi = imp.getFileInfo(); + } + + /** Resaves the image. Calls saveAsTiff() if this is a new image, not a TIFF, + or if the image was loaded using a URL. Returns false if saveAsTiff() is + called and the user selects cancel in the file save dialog box. */ + public boolean save() { + FileInfo ofi = null; + if (imp!=null) ofi = imp.getOriginalFileInfo(); + boolean validName = ofi!=null && imp.getTitle().equals(ofi.fileName); + if (validName && ofi.fileFormat==FileInfo.TIFF && ofi.directory!=null && !ofi.directory.equals("") && (ofi.url==null||ofi.url.equals(""))) { + name = imp.getTitle(); + directory = ofi.directory; + String path = directory+name; + File f = new File(path); + if (f==null || !f.exists()) + return saveAsTiff(); + if (!IJ.isMacro()) { + GenericDialog gd = new GenericDialog("Save as TIFF"); + gd.addMessage("\""+ofi.fileName+"\" already exists.\nDo you want to replace it?"); + gd.setOKLabel("Replace"); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + } + IJ.showStatus("Saving "+path); + if (imp.getStackSize()>1) { + IJ.saveAs(imp, "tif", path); + return true; + } else + return saveAsTiff(path); + } else + return saveAsTiff(); + } + + String getPath(String type, String extension) { + name = imp.getTitle(); + SaveDialog sd = new SaveDialog("Save as "+type, name, extension); + name = sd.getFileName(); + if (name==null) + return null; + directory = sd.getDirectory(); + imp.startTiming(); + String path = directory+name; + return path; + } + + /** Saves the image or stack in TIFF format using a save file + dialog. Returns false if the user selects cancel. Equivalent to + IJ.saveAsTiff(imp,""), which is more convenient. */ + public boolean saveAsTiff() { + String path = getPath("TIFF", ".tif"); + if (path==null) + return false; + if (fi.nImages>1) + return saveAsTiffStack(path); + else + return saveAsTiff(path); + } + + /** Saves the image in TIFF format using the specified path. Equivalent to + IJ.saveAsTiff(imp,path), which is more convenient. */ + public boolean saveAsTiff(String path) { + if (fi.nImages>1) + return saveAsTiffStack(path); + if (imp.getProperty("FHT")!=null && path.contains("FFT of ")) + setupFFTSave(); + fi.info = imp.getInfoProperty(); + String label = imp.isStack()?imp.getStack().getSliceLabel(1):null; + if (label!=null) { + fi.sliceLabels = new String[1]; + fi.sliceLabels[0] = label; + } + fi.description = getDescriptionString(); + if (imp.getProperty(Plot.PROPERTY_KEY) != null) { + Plot plot = (Plot)(imp.getProperty(Plot.PROPERTY_KEY)); + fi.plot = plot.toByteArray(); + } + fi.roi = RoiEncoder.saveAsByteArray(imp.getRoi()); + fi.overlay = getOverlay(imp); + fi.properties = imp.getPropertiesAsArray(); + DataOutputStream out = null; + try { + TiffEncoder file = new TiffEncoder(fi); + out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(path),bsize)); + file.write(out); + out.close(); + } catch (IOException e) { + showErrorMessage("saveAsTiff", path, e); + return false; + } finally { + if (out!=null) + try {out.close();} catch (IOException e) {} + } + updateImp(fi, FileInfo.TIFF); + return true; + } + + private void setupFFTSave() { + Object obj = imp.getProperty("FHT"); + if (obj==null) return; + FHT fht = (obj instanceof FHT)?(FHT)obj:null; + if (fht==null) return; + if (fht.originalColorModel!=null && fht.originalBitDepth!=24) + fht.setColorModel(fht.originalColorModel); + ImagePlus imp2 = new ImagePlus(imp.getTitle(), fht); + imp2.setProperty("Info", imp.getProperty("Info")); + imp2.setProperties(imp.getPropertiesAsArray()); + imp2.setCalibration(imp.getCalibration()); + imp = imp2; + fi = imp.getFileInfo(); + } + + public static byte[][] getOverlay(ImagePlus imp) { + if (imp.getHideOverlay()) + return null; + Overlay overlay = imp.getOverlay(); + if (overlay==null) { + ImageCanvas ic = imp.getCanvas(); + if (ic==null) return null; + overlay = ic.getShowAllList(); // ROI Manager "Show All" list + if (overlay==null) return null; + } + int n = overlay.size(); + if (n==0) + return null; + if (Orthogonal_Views.isOrthoViewsImage(imp)) + return null; + byte[][] array = new byte[n][]; + for (int i=0; i1 && imp.getStack().isVirtual()) + fi.virtualStack = (VirtualStack)imp.getStack(); + DataOutputStream out = null; + try { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(path)); + out = new DataOutputStream(new BufferedOutputStream(zos,bsize)); + zos.putNextEntry(new ZipEntry(name)); + TiffEncoder te = new TiffEncoder(fi); + te.write(out); + out.close(); + } + catch (IOException e) { + showErrorMessage("saveAsZip", path, e); + return false; + } finally { + if (out!=null) + try {out.close();} catch (IOException e) {} + } + updateImp(fi, FileInfo.TIFF); + return true; + } + + public static boolean okForGif(ImagePlus imp) { + if (imp.getType()==ImagePlus.COLOR_RGB) + return false; + else + return true; + } + + /** Save the image in GIF format using a save file + dialog. Returns false if the user selects cancel + or the image is not 8-bits. */ + public boolean saveAsGif() { + String path = getPath("GIF", ".gif"); + if (path==null) + return false; + else + return saveAsGif(path); + } + + /** Save the image in Gif format using the specified path. Returns + false if the image is not 8-bits or there is an I/O error. */ + public boolean saveAsGif(String path) { + IJ.runPlugIn(imp, "ij.plugin.GifWriter", path); + updateImp(fi, FileInfo.GIF_OR_JPG); + return true; + } + + /** Always returns true. */ + public static boolean okForJpeg(ImagePlus imp) { + return true; + } + + /** Save the image in JPEG format using a save file + dialog. Returns false if the user selects cancel. + @see #setJpegQuality + @see #getJpegQuality + */ + public boolean saveAsJpeg() { + String type = "JPEG ("+getJpegQuality()+")"; + String path = getPath(type, ".jpg"); + if (path==null) + return false; + else + return saveAsJpeg(path); + } + + /** Save the image in JPEG format using the specified path. + @see #setJpegQuality + @see #getJpegQuality + */ + public boolean saveAsJpeg(String path) { + String err = JpegWriter.save(imp, path, jpegQuality); + if (err==null && !(imp.getType()==ImagePlus.GRAY16 || imp.getType()==ImagePlus.GRAY32)) + updateImp(fi, FileInfo.GIF_OR_JPG); + return true; + } + + /** Save the image in BMP format using a save file dialog. + Returns false if the user selects cancel. */ + public boolean saveAsBmp() { + String path = getPath("BMP", ".bmp"); + if (path==null) + return false; + else + return saveAsBmp(path); + } + + /** Save the image in BMP format using the specified path. */ + public boolean saveAsBmp(String path) { + IJ.runPlugIn(imp, "ij.plugin.BMP_Writer", path); + updateImp(fi, FileInfo.BMP); + return true; + } + + /** Saves grayscale images in PGM (portable graymap) format + and RGB images in PPM (portable pixmap) format, + using a save file dialog. + Returns false if the user selects cancel. + */ + public boolean saveAsPgm() { + String extension = imp.getBitDepth()==24?".pnm":".pgm"; + String path = getPath("PGM", extension); + if (path==null) + return false; + else + return saveAsPgm(path); + } + + /** Saves grayscale images in PGM (portable graymap) format + and RGB images in PPM (portable pixmap) format, + using the specified path. */ + public boolean saveAsPgm(String path) { + IJ.runPlugIn(imp, "ij.plugin.PNM_Writer", path); + updateImp(fi, FileInfo.PGM); + return true; + } + + /** Save the image in PNG format using a save file dialog. + Returns false if the user selects cancel. */ + public boolean saveAsPng() { + String path = getPath("PNG", ".png"); + if (path==null) + return false; + else + return saveAsPng(path); + } + + /** Save the image in PNG format using the specified path. */ + public boolean saveAsPng(String path) { + IJ.runPlugIn(imp, "ij.plugin.PNG_Writer", path); + updateImp(fi, FileInfo.IMAGEIO); + return true; + } + + /** Save the image in FITS format using a save file dialog. + Returns false if the user selects cancel. */ + public boolean saveAsFits() { + if (!okForFits(imp)) return false; + String path = getPath("FITS", ".fits"); + if (path==null) + return false; + else + return saveAsFits(path); + } + + /** Save the image in FITS format using the specified path. */ + public boolean saveAsFits(String path) { + if (!okForFits(imp)) return false; + IJ.runPlugIn(imp, "ij.plugin.FITS_Writer", path); + updateImp(fi, FileInfo.FITS); + return true; + } + + public static boolean okForFits(ImagePlus imp) { + if (imp.getBitDepth()==24) { + IJ.error("FITS Writer", "Grayscale image required"); + return false; + } else + return true; + } + + /** Save the image or stack as raw data using a save file + dialog. Returns false if the user selects cancel. */ + public boolean saveAsRaw() { + String path = getPath("Raw", ".raw"); + if (path==null) + return false; + if (imp.getStackSize()==1) + return saveAsRaw(path); + else + return saveAsRawStack(path); + } + + /** Save the image as raw data using the specified path. */ + public boolean saveAsRaw(String path) { + fi.nImages = 1; + fi.intelByteOrder = Prefs.intelByteOrder; + boolean signed16Bit = false; + short[] pixels = null; + int n = 0; + OutputStream out = null; + try { + signed16Bit = imp.getCalibration().isSigned16Bit(); + if (signed16Bit) { + pixels = (short[])imp.getProcessor().getPixels(); + n = imp.getWidth()*imp.getHeight(); + for (int i=0; i1 && imp.getStack().isVirtual(); + if (virtualStack) { + fi.virtualStack = (VirtualStack)imp.getStack(); + if (imp.getProperty("AnalyzeFormat")!=null) fi.fileName="FlipTheseImages"; + } + OutputStream out = null; + try { + signed16Bit = imp.getCalibration().isSigned16Bit(); + if (signed16Bit && !virtualStack) { + stack = (Object[])fi.pixels; + n = imp.getWidth()*imp.getHeight(); + for (int slice=0; slice100) + msg = msg.substring(0, 100); + msg = "File saving error (IOException):\n \"" + msg + "\""; + IJ.error("FileSaver."+title, msg+" \n "+path); + IJ.showProgress(1.0); + } + + private void error(String msg) { + IJ.error("FileSaver", msg); + } + + /** Returns a string containing information about the specified image. */ + public String getDescriptionString() { + Calibration cal = imp.getCalibration(); + StringBuffer sb = new StringBuffer(100); + sb.append("ImageJ="+ImageJ.VERSION+"\n"); + if (fi.nImages>1 && fi.fileType!=FileInfo.RGB48) + sb.append("images="+fi.nImages+"\n"); + int channels = imp.getNChannels(); + if (channels>1) + sb.append("channels="+channels+"\n"); + int slices = imp.getNSlices(); + if (slices>1) + sb.append("slices="+slices+"\n"); + int frames = imp.getNFrames(); + if (frames>1) + sb.append("frames="+frames+"\n"); + if (imp.isHyperStack()) sb.append("hyperstack=true\n"); + if (imp.isComposite()) { + String mode = ((CompositeImage)imp).getModeAsString(); + sb.append("mode="+mode+"\n"); + } + if (fi.unit!=null) + appendEscapedLine(sb, "unit="+fi.unit); + int bitDepth = imp.getBitDepth(); + if (fi.valueUnit!=null && (fi.calibrationFunction!=Calibration.CUSTOM||bitDepth==32)) { + if (bitDepth!=32) { + sb.append("cf="+fi.calibrationFunction+"\n"); + if (fi.coefficients!=null) { + for (int i=0; i=0x20 && c<0x7f && c!='\\') + sb.append(c); + else if (c<=0xffff) { //(supplementary unicode characters >0xffff unsupported) + sb.append("\\u"); + sb.append(Tools.int2hex(c, 4)); + } + } + sb.append('\n'); + } + + /** Specifies the image quality (0-100). 0 is poorest image quality, + highest compression, and 100 is best image quality, lowest compression. */ + public static void setJpegQuality(int quality) { + jpegQuality = quality; + if (jpegQuality<0) jpegQuality = 0; + if (jpegQuality>100) jpegQuality = 100; + } + + /** Returns the current JPEG quality setting (0-100). */ + public static int getJpegQuality() { + return jpegQuality; + } + + /** Sets the BufferedOutputStream buffer size in bytes (default is 32K). */ + public static void setBufferSize(int bufferSize) { + bsize = bufferSize; + if (bsize<2048) bsize = 2048; + } + +} diff --git a/src/ij/io/ImageReader.java b/src/ij/io/ImageReader.java new file mode 100644 index 0000000..ed929b1 --- /dev/null +++ b/src/ij/io/ImageReader.java @@ -0,0 +1,1069 @@ +package ij.io; +import ij.*; +import ij.process.*; +import java.io.*; +import java.net.*; +import java.awt.image.BufferedImage; +import javax.imageio.ImageIO; +import java.util.zip.Inflater; +import java.util.zip.DataFormatException; + + +/** Reads raw 8-bit, 16-bit or 32-bit (float or RGB) + images from a stream or URL. */ +public class ImageReader { + + private static final int CLEAR_CODE = 256; + private static final int EOI_CODE = 257; + + private FileInfo fi; + private int width, height; + private long skipCount; + private int bytesPerPixel, bufferSize, nPixels; + private long byteCount; + private boolean showProgressBar=true; + private int eofErrorCount; + private int imageCount; + private long startTime; + public double min, max; // readRGB48() calculates min/max pixel values + + /** + Constructs a new ImageReader using a FileInfo object to describe the file to be read. + @see ij.io.FileInfo + */ + public ImageReader (FileInfo fi) { + this.fi = fi; + width = fi.width; + height = fi.height; + skipCount = fi.getOffset(); + } + + void eofError() { + eofErrorCount++; + } + + byte[] read8bitImage(InputStream in) throws IOException { + if (fi.compression>FileInfo.COMPRESSION_NONE) + return readCompressed8bitImage(in); + byte[] pixels = new byte[nPixels]; + // assume contiguous strips + int count, actuallyRead; + int totalRead = 0; + while (totalReadbyteCount) + count = (int)(byteCount-totalRead); + else + count = bufferSize; + actuallyRead = in.read(pixels, totalRead, count); + if (actuallyRead==-1) {eofError(); break;} + totalRead += actuallyRead; + showProgress(totalRead, byteCount); + } + return pixels; + } + + byte[] readCompressed8bitImage(InputStream in) throws IOException { + byte[] pixels = new byte[nPixels]; + int current = 0; + byte last = 0; + for (int i=0; i 0) { + long skip = (fi.stripOffsets[i]&0xffffffffL) - (fi.stripOffsets[i-1]&0xffffffffL) - fi.stripLengths[i-1]; + if (skip > 0L) in.skip(skip); + } + byte[] byteArray = new byte[fi.stripLengths[i]]; + int read = 0, left = byteArray.length; + while (left > 0) { + int r = in.read(byteArray, read, left); + if (r == -1) {eofError(); break;} + read += r; + left -= r; + } + byteArray = uncompress(byteArray); + int length = byteArray.length; + length = length - (length%fi.width); + if (fi.compression==FileInfo.LZW_WITH_DIFFERENCING) { + for (int b=0; bpixels.length) + length = pixels.length-current; + System.arraycopy(byteArray, 0, pixels, current, length); + current += length; + showProgress(i+1, fi.stripOffsets.length); + } + return pixels; + } + + /** Reads a 16-bit image. Signed pixels are converted to unsigned by adding 32768. */ + short[] read16bitImage(InputStream in) throws IOException { + if (fi.compression>FileInfo.COMPRESSION_NONE || (fi.stripOffsets!=null&&fi.stripOffsets.length>1) && fi.fileType!=FileInfo.RGB48_PLANAR) + return readCompressed16bitImage(in); + int pixelsRead; + byte[] buffer = new byte[bufferSize]; + short[] pixels = new short[nPixels]; + long totalRead = 0L; + int base = 0; + int count, value; + int bufferCount; + + while (totalReadbyteCount) + bufferSize = (int)(byteCount-totalRead); + bufferCount = 0; + while (bufferCount 0) { + long skip = (fi.stripOffsets[k]&0xffffffffL) - (fi.stripOffsets[k-1]&0xffffffffL) - fi.stripLengths[k-1]; + if (skip > 0L) in.skip(skip); + } + byte[] byteArray = new byte[fi.stripLengths[k]]; + int read = 0, left = byteArray.length; + while (left > 0) { + int r = in.read(byteArray, read, left); + if (r == -1) {eofError(); break;} + read += r; + left -= r; + } + byteArray = uncompress(byteArray); + int pixelsRead = byteArray.length/bytesPerPixel; + pixelsRead = pixelsRead - (pixelsRead%fi.width); + int pmax = base+pixelsRead; + if (pmax > nPixels) pmax = nPixels; + if (fi.intelByteOrder) { + for (int i=base,j=0; iFileInfo.COMPRESSION_NONE || (fi.stripOffsets!=null&&fi.stripOffsets.length>1)) + return readCompressed32bitImage(in); + int pixelsRead; + byte[] buffer = new byte[bufferSize]; + float[] pixels = new float[nPixels]; + long totalRead = 0L; + int base = 0; + int count, value; + int bufferCount; + int tmp; + + while (totalReadbyteCount) + bufferSize = (int)(byteCount-totalRead); + bufferCount = 0; + while (bufferCountnPixels) pmax = nPixels; + int j = 0; + if (fi.intelByteOrder) + for (int i=base; i 0) { + long skip = (fi.stripOffsets[k]&0xffffffffL) - (fi.stripOffsets[k-1]&0xffffffffL) - fi.stripLengths[k-1]; + if (skip > 0L) in.skip(skip); + } + byte[] byteArray = new byte[fi.stripLengths[k]]; + int read = 0, left = byteArray.length; + while (left > 0) { + int r = in.read(byteArray, read, left); + if (r == -1) {eofError(); break;} + read += r; + left -= r; + } + byteArray = uncompress(byteArray); + int pixelsRead = byteArray.length/bytesPerPixel; + pixelsRead = pixelsRead - (pixelsRead%fi.width); + int pmax = base+pixelsRead; + if (pmax > nPixels) pmax = nPixels; + int tmp; + if (fi.intelByteOrder) { + for (int i=base,j=0; ibyteCount) + bufferSize = (int)(byteCount-totalRead); + bufferCount = 0; + while (bufferCountFileInfo.COMPRESSION_NONE || (fi.stripOffsets!=null&&fi.stripOffsets.length>1)) + return readCompressedChunkyRGB(in); + int pixelsRead; + bufferSize = 24*width; + byte[] buffer = new byte[bufferSize]; + int[] pixels = new int[nPixels]; + long totalRead = 0L; + int base = 0; + int count, value; + int bufferCount; + int r, g, b, a; + + while (totalReadbyteCount) + bufferSize = (int)(byteCount-totalRead); + bufferCount = 0; + while (bufferCount0) { // if k>0 then c=c*(1-k)+k + r = ((r*(256 - a))>>8) + a; + g = ((g*(256 - a))>>8) + a; + b = ((b*(256 - a))>>8) + a; + } // else r=1-c, g=1-m and b=1-y, which IJ does by inverting image + } else { // ARGB + r = buffer[j++]&0xff; + g = buffer[j++]&0xff; + b = buffer[j++]&0xff; + j++; // ignore alfa byte + } + } else { + r = buffer[j++]&0xff; + g = buffer[j++]&0xff; + b = buffer[j++]&0xff; + } + if (bgr) + pixels[i] = 0xff000000 | (b<<16) | (g<<8) | r; + else + pixels[i] = 0xff000000 | (r<<16) | (g<<8) | b; + } + base += pixelsRead; + } + return pixels; + } + + int[] readCompressedChunkyRGB(InputStream in) throws IOException { + int[] pixels = new int[nPixels]; + int base = 0; + int lastRed=0, lastGreen=0, lastBlue=0; + int nextByte; + int red=0, green=0, blue=0, alpha = 0; + boolean bgr = fi.fileType==FileInfo.BGR; + boolean cmyk = fi.fileType==FileInfo.CMYK; + boolean differencing = fi.compression == FileInfo.LZW_WITH_DIFFERENCING; + for (int i=0; i 0) { + long skip = (fi.stripOffsets[i]&0xffffffffL) - (fi.stripOffsets[i-1]&0xffffffffL) - fi.stripLengths[i-1]; + if (skip > 0L) in.skip(skip); + } + byte[] byteArray = new byte[fi.stripLengths[i]]; + int read = 0, left = byteArray.length; + while (left > 0) { + int r = in.read(byteArray, read, left); + if (r == -1) {eofError(); break;} + read += r; + left -= r; + } + byteArray = uncompress(byteArray); + if (differencing) { + for (int b=0; b nPixels) pmax = nPixels; + for (int j=base; j0) { + red = ((red*(256-alpha))>>8) + alpha; + green = ((green*(256-alpha))>>8) + alpha; + blue = ((blue*(256-alpha))>>8) + alpha; + } + } else { + red = byteArray[k++]&0xff; + green = byteArray[k++]&0xff; + blue = byteArray[k++]&0xff; + } + if (bgr) + pixels[j] = 0xff000000 | (blue<<16) | (green<<8) | red; + else + pixels[j] = 0xff000000 | (red<<16) | (green<<8) | blue; + } + base += pixelsRead; + showProgress(i+1, fi.stripOffsets.length); + } + return pixels; + } + + int[] readJPEG(InputStream in) throws IOException { + BufferedImage bi = ImageIO.read(in); + ImageProcessor ip = new ColorProcessor(bi); + return (int[])ip.getPixels(); + } + + int[] readPlanarRGB(InputStream in) throws IOException { + if (fi.compression>FileInfo.COMPRESSION_NONE || (fi.stripOffsets!=null&&fi.stripOffsets.length>1)) + return readCompressedPlanarRGBImage(in); + DataInputStream dis = new DataInputStream(in); + int planeSize = nPixels; // 1/3 image size + byte[] buffer = new byte[planeSize]; + int[] pixels = new int[nPixels]; + int r, g, b; + + startTime = 0L; + showProgress(10, 100); + dis.readFully(buffer); + for (int i=0; i < planeSize; i++) { + r = buffer[i]&0xff; + pixels[i] = 0xff000000 | (r<<16); + } + + showProgress(40, 100); + dis.readFully(buffer); + for (int i=0; i < planeSize; i++) { + g = buffer[i]&0xff; + pixels[i] |= g<<8; + } + + showProgress(70, 100); + dis.readFully(buffer); + for (int i=0; i < planeSize; i++) { + b = buffer[i]&0xff; + pixels[i] |= b; + } + + showProgress(90, 100); + return pixels; + } + + int[] readCompressedPlanarRGBImage(InputStream in) throws IOException { + int[] pixels = new int[nPixels]; + int r, g, b; + nPixels *= 3; // read all 3 planes + byte[] buffer = readCompressed8bitImage(in); + nPixels /= 3; + for (int i=0; i500L) + IJ.showProgress(current, last); + } + + private void showProgress(long current, long last) { + showProgress((int)(current/10L), (int)(last/10L)); + } + + Object readRGB48(InputStream in) throws IOException { + if (fi.compression>FileInfo.COMPRESSION_NONE) + return readCompressedRGB48(in); + int channels = fi.samplesPerPixel; + if (channels==1) channels=3; + short[][] stack = new short[channels][nPixels]; + DataInputStream dis = new DataInputStream(in); + int pixel = 0; + int min=65535, max=0; + if (fi.stripLengths==null) { + fi.stripLengths = new int[fi.stripOffsets.length]; + fi.stripLengths[0] = width*height*bytesPerPixel; + } + for (int i=0; i0) { + long skip = (fi.stripOffsets[i]&0xffffffffL) - (fi.stripOffsets[i-1]&0xffffffffL) - fi.stripLengths[i-1]; + if (skip>0L) dis.skip(skip); + } + int len = fi.stripLengths[i]; + int bytesToGo = (nPixels-pixel)*channels*2; + if (len>bytesToGo) len = bytesToGo; + byte[] buffer = new byte[len]; + dis.readFully(buffer); + int value; + int channel=0; + boolean intel = fi.intelByteOrder; + for (int base=0; basemax) + max = value; + stack[channel][pixel] = (short)(value); + channel++; + if (channel==channels) { + channel = 0; + pixel++; + } + } + showProgress(i+1, fi.stripOffsets.length); + } + this.min=min; this.max=max; + return stack; + } + + Object readCompressedRGB48(InputStream in) throws IOException { + if (fi.compression==FileInfo.LZW_WITH_DIFFERENCING) + throw new IOException("ImageJ cannot open 48-bit LZW compressed TIFFs with predictor"); + int channels = 3; + short[][] stack = new short[channels][nPixels]; + DataInputStream dis = new DataInputStream(in); + int pixel = 0; + int min=65535, max=0; + for (int i=0; i0) { + long skip = (fi.stripOffsets[i]&0xffffffffL) - (fi.stripOffsets[i-1]&0xffffffffL) - fi.stripLengths[i-1]; + if (skip>0L) dis.skip(skip); + } + int len = fi.stripLengths[i]; + byte[] buffer = new byte[len]; + dis.readFully(buffer); + buffer = uncompress(buffer); + len = buffer.length; + if (len % 2 != 0) len--; + int value; + int channel=0; + boolean intel = fi.intelByteOrder; + for (int base=0; basemax) + max = value; + stack[channel][pixel] = (short)(value); + channel++; + if (channel==channels) { + channel = 0; + pixel++; + } + } + showProgress(i+1, fi.stripOffsets.length); + } + this.min=min; this.max=max; + return stack; + } + + Object readRGB48Planar(InputStream in) throws IOException { + int channels = fi.samplesPerPixel; + if (channels==1) channels=3; + Object[] stack = new Object[channels]; + for (int i=0; i>4)&0xf)); + count++; + if (count==width) break; + pixels[index2+count] = (short)(((buffer[index1+1]&0xf)*256) + (buffer[index1+2]&0xff)); + count++; index1+=3; + } + } + return pixels; + } + + float[] read24bitImage(InputStream in) throws IOException { + byte[] buffer = new byte[width*3]; + float[] pixels = new float[nPixels]; + int b1, b2, b3; + DataInputStream dis = new DataInputStream(in); + for (int y=0; y=0; i--) { + value2 = (value1&(1<0) { + long bytesRead = 0; + int skipAttempts = 0; + long count; + while (bytesRead5) break; + bytesRead += count; + } + } + byteCount = ((long)width)*height*bytesPerPixel; + if (fi.fileType==FileInfo.BITMAP) { + int scan=width/8, pad = width%8; + if (pad>0) scan++; + byteCount = scan*height; + } + nPixels = width*height; + bufferSize = (int)(byteCount/25L); + if (bufferSize<8192) + bufferSize = 8192; + else + bufferSize = (bufferSize/8192)*8192; + } + + /** + Reads the image from the InputStream and returns the pixel + array (byte, short, int or float). Returns null if there + was an IO exception. Does not close the InputStream. + */ + public Object readPixels(InputStream in) { + Object pixels; + startTime = System.currentTimeMillis(); + try { + switch (fi.fileType) { + case FileInfo.GRAY8: + case FileInfo.COLOR8: + bytesPerPixel = 1; + skip(in); + pixels = (Object)read8bitImage(in); + break; + case FileInfo.GRAY16_SIGNED: + case FileInfo.GRAY16_UNSIGNED: + bytesPerPixel = 2; + skip(in); + pixels = (Object)read16bitImage(in); + break; + case FileInfo.GRAY32_INT: + case FileInfo.GRAY32_UNSIGNED: + case FileInfo.GRAY32_FLOAT: + bytesPerPixel = 4; + skip(in); + pixels = (Object)read32bitImage(in); + break; + case FileInfo.GRAY64_FLOAT: + bytesPerPixel = 8; + skip(in); + pixels = (Object)read64bitImage(in); + break; + case FileInfo.RGB: + case FileInfo.BGR: + case FileInfo.ARGB: + case FileInfo.ABGR: + case FileInfo.BARG: + case FileInfo.CMYK: + bytesPerPixel = fi.getBytesPerPixel(); + skip(in); + pixels = (Object)readChunkyRGB(in); + break; + case FileInfo.RGB_PLANAR: + if (!(in instanceof RandomAccessStream) && fi.stripOffsets!=null && fi.stripOffsets.length>1) + in = new RandomAccessStream(in); + bytesPerPixel = 3; + skip(in); + pixels = (Object)readPlanarRGB(in); + break; + case FileInfo.BITMAP: + bytesPerPixel = 1; + skip(in); + pixels = (Object)read1bitImage(in); + break; + case FileInfo.RGB48: + bytesPerPixel = 6; + skip(in); + pixels = (Object)readRGB48(in); + break; + case FileInfo.RGB48_PLANAR: + bytesPerPixel = 2; + skip(in); + pixels = (Object)readRGB48Planar(in); + break; + case FileInfo.GRAY12_UNSIGNED: + skip(in); + short[] data = read12bitImage(in); + pixels = (Object)data; + break; + case FileInfo.GRAY24_UNSIGNED: + skip(in); + pixels = (Object)read24bitImage(in); + break; + default: + pixels = null; + } + showProgress(1, 1); + imageCount++; + return pixels; + } + catch (IOException e) { + IJ.log("" + e); + return null; + } + } + + /** + Skips the specified number of bytes, then reads an image and + returns the pixel array (byte, short, int or float). Returns + null if there was an IO exception. Does not close the InputStream. + */ + public Object readPixels(InputStream in, long skipCount) { + this.skipCount = skipCount; + showProgressBar = false; + Object pixels = readPixels(in); + if (eofErrorCount>(imageCount==1?1:0)) + return null; + else + return pixels; + } + + /** + Reads the image from a URL and returns the pixel array (byte, + short, int or float). Returns null if there was an IO exception. + */ + public Object readPixels(String url) { + java.net.URL theURL; + InputStream is; + try {theURL = new URL(url);} + catch (MalformedURLException e) {IJ.log(""+e); return null;} + try {is = theURL.openStream();} + catch (IOException e) {IJ.log(""+e); return null;} + return readPixels(is); + } + + private byte[] uncompress(byte[] input) { + if (fi.compression==FileInfo.PACK_BITS) + return packBitsUncompress(input, fi.rowsPerStrip*fi.width*fi.getBytesPerPixel()); + else if (fi.compression==FileInfo.LZW || fi.compression==FileInfo.LZW_WITH_DIFFERENCING) + return lzwUncompress(input); + else if (fi.compression==FileInfo.ZIP) + return zipUncompress(input); + else + return input; + } + + /** TIFF Adobe ZIP support contributed by Jason Newton. */ + public byte[] zipUncompress(byte[] input) { + ByteArrayOutputStream imageBuffer = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + Inflater decompressor = new Inflater(); + decompressor.setInput(input); + try { + while(!decompressor.finished()) { + int rlen = decompressor.inflate(buffer); + imageBuffer.write(buffer, 0, rlen); + } + } catch(DataFormatException e){ + IJ.log(e.toString()); + } + decompressor.end(); + return imageBuffer.toByteArray(); + } + + /** + * Utility method for decoding an LZW-compressed image strip. + * Adapted from the TIFF 6.0 Specification: + * http://partners.adobe.com/asn/developer/pdfs/tn/TIFF6.pdf (page 61) + * Author: Curtis Rueden (ctrueden at wisc.edu) + */ + public byte[] lzwUncompress(byte[] input) { + if (input==null || input.length==0) + return input; + byte[][] symbolTable = new byte[16384][1]; + int bitsToRead = 9; + int nextSymbol = 258; + int code; + int oldCode = -1; + ByteVector out = new ByteVector(8192); + BitBuffer bb = new BitBuffer(input); + byte[] byteBuffer1 = new byte[16]; + byte[] byteBuffer2 = new byte[16]; + + while (out.size()=0) { // 0 <= n <= 127 + byte[] b = new byte[n+1]; + for (int i=0; isize) + count = (int)(size-bytesWritten); + int j = (int)(bytesWritten/2L); + int value; + if (fi.intelByteOrder) + for (int i=0; i < count; i+=2) { + value = pixels[j]; + buffer[i] = (byte)value; + buffer[i+1] = (byte)(value>>>8); + j++; + } + else + for (int i=0; i < count; i+=2) { + value = pixels[j]; + buffer[i] = (byte)(value>>>8); + buffer[i+1] = (byte)value; + j++; + } + out.write(buffer, 0, count); + bytesWritten += count; + showProgress((double)bytesWritten/size); + } + } + + void write16BitStack(OutputStream out, Object[] stack) throws IOException { + showProgressBar = false; + for (int i=0; i>>8); + value = g[index1]; + buffer[index2++] = (byte)value; + buffer[index2++] = (byte)(value>>>8); + value = b[index1]; + buffer[index2++] = (byte)value; + buffer[index2++] = (byte)(value>>>8); + index1++; + } + } else { + for (int i=0; i>>8); + buffer[index2++] = (byte)value; + value = g[index1]; + buffer[index2++] = (byte)(value>>>8); + buffer[index2++] = (byte)value; + value = b[index1]; + buffer[index2++] = (byte)(value>>>8); + buffer[index2++] = (byte)value; + index1++; + } + } + out.write(buffer, 0, count); + } + } + + void writeFloatImage(OutputStream out, float[] pixels) throws IOException { + long bytesWritten = 0L; + long size = 4L*fi.width*fi.height; + int count = getCount(size); + byte[] buffer = new byte[count]; + int tmp; + + while (bytesWrittensize) + count = (int)(size-bytesWritten); + int j = (int)(bytesWritten/4L); + if (fi.intelByteOrder) + for (int i=0; i < count; i+=4) { + tmp = Float.floatToRawIntBits(pixels[j]); + buffer[i] = (byte)tmp; + buffer[i+1] = (byte)(tmp>>8); + buffer[i+2] = (byte)(tmp>>16); + buffer[i+3] = (byte)(tmp>>24); + j++; + } + else + for (int i=0; i < count; i+=4) { + tmp = Float.floatToRawIntBits(pixels[j]); + buffer[i] = (byte)(tmp>>24); + buffer[i+1] = (byte)(tmp>>16); + buffer[i+2] = (byte)(tmp>>8); + buffer[i+3] = (byte)tmp; + j++; + } + out.write(buffer, 0, count); + bytesWritten += count; + showProgress((double)bytesWritten/size); + } + } + + private int getCount(long imageSize) { + if (savingStack || imageSize<4L) + return (int)imageSize; + int count = (int)(imageSize/50L); + if (count<65536) + count = 65536; + if (count>imageSize) + count = (int)imageSize; + count = (count/4)*4; + if (IJ.debugMode) IJ.log("ImageWriter: "+imageSize+" "+count+" "+imageSize/50); + return count; + } + + void writeFloatStack(OutputStream out, Object[] stack) throws IOException { + showProgressBar = false; + for (int i=0; isize) + count = (int)(size-bytesWritten); + int j = (int)(bytesWritten/3L); + for (int i=0; i>16); //red + buffer[i+1] = (byte)(pixels[j]>>8); //green + buffer[i+2] = (byte)pixels[j]; //blue + j++; + } + out.write(buffer, 0, count); + bytesWritten += count; + showProgress((double)bytesWritten/size); + } + } + + void writeRGBStack(OutputStream out, Object[] stack) throws IOException { + showProgressBar = false; + for (int i=0; i1 + then fi.pixels must be a 2D array, for example an + array of images returned by ImageStack.getImageArray()). + The fi.offset field is ignored. */ + public void write(OutputStream out) throws IOException { + if (fi.pixels==null && fi.virtualStack==null) + throw new IOException("ImageWriter: fi.pixels==null"); + if (fi.nImages>1 && fi.virtualStack==null && !(fi.pixels instanceof Object[])) + throw new IOException("ImageWriter: fi.pixels not a stack"); + if (fi.width*fi.height*fi.getBytesPerPixel()<26214400) + showProgressBar = false; // don't show progress bar if image<25MB + switch (fi.fileType) { + case FileInfo.GRAY8: + case FileInfo.COLOR8: + if (fi.nImages>1 && fi.virtualStack!=null) + write8BitVirtualStack(out, fi.virtualStack); + else if (fi.nImages>1) + write8BitStack(out, (Object[])fi.pixels); + else + write8BitImage(out, (byte[])fi.pixels); + break; + case FileInfo.GRAY16_SIGNED: + case FileInfo.GRAY16_UNSIGNED: + if (fi.nImages>1 && fi.virtualStack!=null) + write16BitVirtualStack(out, fi.virtualStack); + else if (fi.nImages>1) + write16BitStack(out, (Object[])fi.pixels); + else + write16BitImage(out, (short[])fi.pixels); + break; + case FileInfo.RGB48: + writeRGB48Image(out, (Object[])fi.pixels); + break; + case FileInfo.GRAY32_FLOAT: + if (fi.nImages>1 && fi.virtualStack!=null) + writeFloatVirtualStack(out, fi.virtualStack); + else if (fi.nImages>1) + writeFloatStack(out, (Object[])fi.pixels); + else + writeFloatImage(out, (float[])fi.pixels); + break; + case FileInfo.RGB: + if (fi.nImages>1 && fi.virtualStack!=null) + writeRGBVirtualStack(out, fi.virtualStack); + else if (fi.nImages>1) + writeRGBStack(out, (Object[])fi.pixels); + else + writeRGBImage(out, (int[])fi.pixels); + break; + default: + } + savingStack = false; + } + +} + diff --git a/src/ij/io/ImportDialog.java b/src/ij/io/ImportDialog.java new file mode 100644 index 0000000..d5e328e --- /dev/null +++ b/src/ij/io/ImportDialog.java @@ -0,0 +1,362 @@ +package ij.io; + +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.util.*; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.util.*; +import ij.plugin.frame.Recorder; +import ij.plugin.*; +import ij.measure.Calibration; + + +/** This is a dialog box used to imports raw 8, 16, 24 and 32-bit images. */ +public class ImportDialog { + private String fileName; + private String directory; + static final String TYPE = "raw.type"; + static final String WIDTH = "raw.width"; + static final String HEIGHT = "raw.height"; + static final String OFFSET = "raw.offset"; + static final String N = "raw.n"; + static final String GAP = "raw.gap"; + static final String OPTIONS = "raw.options"; + static final int WHITE_IS_ZERO = 1; + static final int INTEL_BYTE_ORDER = 2; + static final int OPEN_ALL = 4; + + // default settings + private static int sChoiceSelection = Prefs.getInt(TYPE,0); + private static int sWidth = Prefs.getInt(WIDTH,512); + private static int sHeight = Prefs.getInt(HEIGHT,512); + private static long sOffset = Prefs.getInt(OFFSET,0); + private static int sNImages = Prefs.getInt(N,1); + private static long sGapBetweenImages = Prefs.getInt(GAP,0); + private static boolean sWhiteIsZero; + private static boolean sIntelByteOrder; + private static boolean sVirtual; + private int choiceSelection = sChoiceSelection; + private int width = sWidth; + private int height = sHeight; + private long offset = sOffset; + private int nImages = sNImages; + private long gapBetweenImages = sGapBetweenImages; + private boolean whiteIsZero = sWhiteIsZero; + private boolean intelByteOrder = sIntelByteOrder; + private boolean virtual = sVirtual; + + private static int options; + private static FileInfo lastFileInfo; + private boolean openAll; + private static String[] types = {"8-bit", "16-bit Signed", "16-bit Unsigned", + "32-bit Signed", "32-bit Unsigned", "32-bit Real", "64-bit Real", "24-bit RGB", + "24-bit RGB Planar", "24-bit BGR", "24-bit Integer", "32-bit ARGB", "32-bit ABGR", "1-bit Bitmap"}; + + static { + options = Prefs.getInt(OPTIONS, 0); + sWhiteIsZero = (options&WHITE_IS_ZERO)!=0; + sIntelByteOrder = (options&INTEL_BYTE_ORDER)!=0; + } + + public ImportDialog(String fileName, String directory) { + this.fileName = fileName; + this.directory = directory; + IJ.showStatus("Importing: " + fileName); + } + + public ImportDialog() { + } + + boolean showDialog() { + boolean macro = Macro.getOptions()!=null; + if (macro) { + width = height = 512; + offset = gapBetweenImages = 0; + nImages = 1; + whiteIsZero = intelByteOrder = virtual = false; + } + if (choiceSelection>=types.length) + choiceSelection = 0; + getDimensionsFromName(fileName); + GenericDialog gd = new GenericDialog("Import>Raw..."); + gd.addChoice("Image type:", types, types[choiceSelection]); + gd.addNumericField("Width:", width, 0, 8, "pixels"); + gd.addNumericField("Height:", height, 0, 8, "pixels"); + gd.addNumericField("Offset to first image:", offset, 0, 8, "bytes"); + gd.addNumericField("Number of images:", nImages, 0, 8, null); + gd.addNumericField("Gap between images:", gapBetweenImages, 0, 8, "bytes"); + gd.addCheckbox("White is zero", whiteIsZero); + gd.addCheckbox("Little-endian byte order", intelByteOrder); + gd.addCheckbox("Open all files in folder", openAll); + gd.addCheckbox("Use virtual stack", virtual); + gd.addHelp(IJ.URL+"/docs/menus/file.html#raw"); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + choiceSelection = gd.getNextChoiceIndex(); + width = (int)gd.getNextNumber(); + height = (int)gd.getNextNumber(); + gd.setSmartRecording(offset==0); + offset = (long)gd.getNextNumber(); + gd.setSmartRecording(nImages==1); + nImages = (int)gd.getNextNumber(); + gd.setSmartRecording(gapBetweenImages==0); + gapBetweenImages = (long)gd.getNextNumber(); + gd.setSmartRecording(false); + whiteIsZero = gd.getNextBoolean(); + intelByteOrder = gd.getNextBoolean(); + openAll = gd.getNextBoolean(); + virtual = gd.getNextBoolean(); + IJ.register(ImportDialog.class); + if (!macro) { + sChoiceSelection = choiceSelection; + sWidth = width; + sHeight = height; + sOffset = offset; + sNImages = nImages; + sGapBetweenImages = gapBetweenImages; + sWhiteIsZero = whiteIsZero; + sIntelByteOrder = intelByteOrder; + sVirtual = virtual; + } + return true; + } + + /** Opens all the images in the directory. */ + void openAll(String[] list, FileInfo fi) { + FolderOpener fo = new FolderOpener(); + list = fo.trimFileList(list); + list = fo.sortFileList(list); + if (list==null) return; + ImageStack stack=null; + ImagePlus imp=null; + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + int digits = 0; + for (int i=0; i99) digits=3; + if (slices>999) digits=4; + if (slices>9999) digits=5; + } + for (int n=1; n<=slices; n++) { + ImageProcessor ip = stack2.getProcessor(n); + if (ip.getMin()max) + max = ip.getMax(); + String label = list[i]; + if (slices>1) label += "-" + IJ.pad(n,digits); + stack.addSlice(label, ip); + } + } catch(OutOfMemoryError e) { + IJ.outOfMemory("OpenAll"); + stack.trim(); + break; + } + IJ.showStatus((stack.size()+1) + ": " + list[i]); + } + } + String dir = Recorder.fixPath(fi.directory); + Recorder.recordCall(fi.getCode()+"imp = Raw.openAll(\""+ dir+"\", fi);"); + if (stack!=null) { + imp = new ImagePlus("Imported Stack", stack); + if (imp.getBitDepth()==16 || imp.getBitDepth()==32) + imp.getProcessor().setMinAndMax(min, max); + Calibration cal = imp.getCalibration(); + if (fi.fileType==FileInfo.GRAY16_SIGNED) + cal.setSigned16BitCalibration(); + imp.show(); + } + } + + /** Displays the dialog and opens the specified image or images. + Does nothing if the dialog is canceled. */ + public void openImage() { + FileInfo fi = getFileInfo(); + if (fi==null) + return; + if (openAll) { + if (virtual) { + ImagePlus imp = Raw.openAllVirtual(directory, fi); + String dir = Recorder.fixPath(directory); + Recorder.recordCall(fi.getCode()+"imp = Raw.openAllVirtual(\""+dir+"\", fi);"); + if (imp!=null) { + imp.setSlice(imp.getStackSize()/2); + imp.show(); + imp.setSlice(1); + } + return; + } + String[] list = new File(directory).list(); + if (list==null) return; + openAll(list, fi); + } else if (virtual) + new FileInfoVirtualStack(fi); + else { + FileOpener fo = new FileOpener(fi); + ImagePlus imp = fo.openImage(); + String filePath = fi.getFilePath(); + filePath = Recorder.fixPath(filePath); + Recorder.recordCall(fi.getCode()+"imp = Raw.open(\""+filePath+"\", fi);"); + if (imp!=null) { + imp.show(); + int n = imp.getStackSize(); + if (n>1) { + imp.setSlice(n/2); + ImageProcessor ip = imp.getProcessor(); + ip.resetMinAndMax(); + imp.setDisplayRange(ip.getMin(),ip.getMax()); + } + } else + IJ.error("File>Import>Raw", "File not found: "+filePath); + } + } + + /** Displays the dialog and returns a FileInfo object that can be used to + open the image. Returns null if the dialog is canceled. The fileName + and directory fields are null if the no argument constructor was used. */ + public FileInfo getFileInfo() { + if (!showDialog()) + return null; + String imageType = types[choiceSelection]; + FileInfo fi = new FileInfo(); + fi.fileFormat = fi.RAW; + fi.fileName = fileName; + directory = IJ.addSeparator(directory); + fi.directory = directory; + fi.width = width; + fi.height = height; + if (offset>2147483647) + fi.longOffset = offset; + else + fi.offset = (int)offset; + fi.nImages = nImages; + fi.gapBetweenImages = (int)gapBetweenImages; + fi.longGap = gapBetweenImages; + fi.intelByteOrder = intelByteOrder; + fi.whiteIsZero = whiteIsZero; + if (imageType.equals("8-bit")) + fi.fileType = FileInfo.GRAY8; + else if (imageType.equals("16-bit Signed")) + fi.fileType = FileInfo.GRAY16_SIGNED; + else if (imageType.equals("16-bit Unsigned")) + fi.fileType = FileInfo.GRAY16_UNSIGNED; + else if (imageType.equals("32-bit Signed")) + fi.fileType = FileInfo.GRAY32_INT; + else if (imageType.equals("32-bit Unsigned")) + fi.fileType = FileInfo.GRAY32_UNSIGNED; + else if (imageType.equals("32-bit Real")) + fi.fileType = FileInfo.GRAY32_FLOAT; + else if (imageType.equals("64-bit Real")) + fi.fileType = FileInfo.GRAY64_FLOAT; + else if (imageType.equals("24-bit RGB")) + fi.fileType = FileInfo.RGB; + else if (imageType.equals("24-bit RGB Planar")) + fi.fileType = FileInfo.RGB_PLANAR; + else if (imageType.equals("24-bit BGR")) + fi.fileType = FileInfo.BGR; + else if (imageType.equals("24-bit Integer")) + fi.fileType = FileInfo.GRAY24_UNSIGNED; + else if (imageType.equals("32-bit ARGB")) + fi.fileType = FileInfo.ARGB; + else if (imageType.equals("32-bit ABGR")) + fi.fileType = FileInfo.ABGR; + else if (imageType.equals("1-bit Bitmap")) + fi.fileType = FileInfo.BITMAP; + else + fi.fileType = FileInfo.GRAY8; + if (IJ.debugMode) IJ.log("ImportDialog: "+fi); + lastFileInfo = (FileInfo)fi.clone(); + return fi; + } + + /** Called once when ImageJ quits. */ + public static void savePreferences(Properties prefs) { + prefs.put(TYPE, Integer.toString(sChoiceSelection)); + prefs.put(WIDTH, Integer.toString(sWidth)); + prefs.put(HEIGHT, Integer.toString(sHeight)); + prefs.put(OFFSET, Integer.toString(sOffset>2147483647?0:(int)sOffset)); + prefs.put(N, Integer.toString(sNImages)); + prefs.put(GAP, Integer.toString(sGapBetweenImages>2147483647?0:(int)sGapBetweenImages)); + int options = 0; + if (sWhiteIsZero) + options |= WHITE_IS_ZERO; + if (sIntelByteOrder) + options |= INTEL_BYTE_ORDER; + prefs.put(OPTIONS, Integer.toString(options)); + } + + /** Returns the FileInfo object used to import the last raw image, + or null if a raw image has not been imported. */ + public static FileInfo getLastFileInfo() { + return lastFileInfo; + } + + private void getDimensionsFromName(String name) { + if (name==null) + return; + if (!name.matches(".*[0-9]+x[0-9]+.*")) + return; // must have 'x' seperator + int lastUnderscore = name.lastIndexOf("_"); + String name2 = name; + if (lastUnderscore>=0) + name2 = name.substring(lastUnderscore); + char[] chars = new char[name2.length()]; + for (int i=0; i2) { + int d = (int)Tools.parseDouble(numbers[2],0); + if (d>0) + nImages = d; + } + guessFormat(directory, name); + } + + private void guessFormat(String dir, String name) { + if (dir==null) return; + File file = new File(dir+name); + long imageSize = (long)width*height*nImages; + long fileSize = file.length(); + if (fileSize==4*imageSize) + choiceSelection = 5; // 32-bit real + else if (fileSize==2*imageSize) + choiceSelection = 2; // 16-bit unsigned + else if (fileSize==3*imageSize) + choiceSelection = 7; // 24-bit RGB + else if (fileSize==imageSize) + choiceSelection = 0; // 8-bit + if (name.endsWith("be.raw")) // big-endian + intelByteOrder = false; + else if (name.endsWith("le.raw")) // little-endian + intelByteOrder = true; + } + +} diff --git a/src/ij/io/LogStream.java b/src/ij/io/LogStream.java new file mode 100644 index 0000000..33d6cac --- /dev/null +++ b/src/ij/io/LogStream.java @@ -0,0 +1,203 @@ +package ij.io; +import ij.IJ; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/** + * This class provides the functionality to divert output sent to the System.out + * and System.err streams to ImageJ's log console. The purpose is to allow + * use of existing Java classes or writing new generic Java classes that only + * output to System.out and are thus less dependent on ImageJ. + * See the ImageJ plugin Redirect_System_Streams at + * http://staff.fh-hagenberg.at/burger/imagej/ + * for usage examples. + * + * @author Wilhelm Burger (wilbur at ieee.org) + * See Also: Redirect_System_Streams (http://staff.fh-hagenberg.at/burger/imagej/) + */ +public class LogStream extends PrintStream { + + private static String outPrefix = "out> "; // prefix string for System.out + private static String errPrefix = "err >"; // prefix string for System.err + + private static PrintStream originalSystemOut = null; + private static PrintStream originalSystemErr = null; + private static PrintStream temporarySystemOut = null; + private static PrintStream temporarySystemErr = null; + + /** + * Redirects all output sent to System.out and System.err to ImageJ's log console + * using the default prefixes. + */ + public static void redirectSystem(boolean redirect) { + if (redirect) + redirectSystem(); + else + revertSystem(); + } + + /** + * Redirects all output sent to System.out and System.err to ImageJ's log console + * using the default prefixes. + * Alternatively use + * {@link #redirectSystemOut(String)} and {@link #redirectSystemErr(String)} + * to redirect the streams separately and to specify individual prefixes. + */ + public static void redirectSystem() { + redirectSystemOut(outPrefix); + redirectSystemErr(errPrefix); + } + + /** + * Redirects all output sent to System.out to ImageJ's log console. + * @param prefix The prefix string inserted at the start of each output line. + * Pass null to use the default prefix or an empty string to + * remove the prefix. + */ + public static void redirectSystemOut(String prefix) { + if (originalSystemOut == null) { // has no effect if System.out is already replaced + originalSystemOut = System.out; // remember the original System.out stream + temporarySystemOut = new LogStream(prefix); + System.setOut(temporarySystemOut); + } + } + + /** + * Redirects all output sent to System.err to ImageJ's log console. + * @param prefix The prefix string inserted at the start of each output line. + * Pass null to use the default prefix or an empty string to + * remove the prefix. + */ + public static void redirectSystemErr(String prefix) { + if (originalSystemErr == null) { // has no effect if System.out is already replaced + originalSystemErr = System.err; // remember the original System.out stream + temporarySystemErr = new LogStream(prefix); + System.setErr(temporarySystemErr); + } + } + + /** + * Returns the redirection stream for {@code System.out} if it exists. + * Note that a reference to the current output stream can also be obtained directly from + * the {@code System.out} field. + * @return A reference to the {@code PrintStream} object currently substituting {@code System.out} + * or {@code null} of if {@code System.out} is currently not redirected. + */ + public static PrintStream getCurrentOutStream() { + return temporarySystemOut; + } + + /** + * Returns the redirection stream for {@code System.err} if it exists. + * Note that a reference to the current output stream can also be obtained directly from + * the {@code System.err} field. + * @return A reference to the {@code PrintStream} object currently substituting {@code System.err} + * or {@code null} of if {@code System.err} is currently not redirected. + */ + public static PrintStream getCurrentErrStream() { + return temporarySystemErr; + } + + /** + * Use this method to revert both System.out and System.err + * to their original output streams. + */ + public static void revertSystem() { + revertSystemOut(); + revertSystemErr(); + } + + /** + * Use this method to revertSystem.out + * to the original output stream. + */ + public static void revertSystemOut() { + if (originalSystemOut != null && temporarySystemOut != null) { + temporarySystemOut.flush(); + temporarySystemOut.close(); + System.setOut(originalSystemOut); + originalSystemOut = null; + temporarySystemOut = null; + } + } + + /** + * Use this method to revertSystem.err + * to the original output stream. + */ + public static void revertSystemErr() { + if (originalSystemErr != null && temporarySystemErr != null) { + temporarySystemErr.flush(); + temporarySystemErr.close(); + System.setErr(originalSystemErr); + originalSystemErr = null; + temporarySystemErr = null; + } + } + + // ---------------------------------------------------------------- + + private final String endOfLineSystem = System.getProperty("line.separator"); + private final String endOfLineShort = String.format("\n"); + private final ByteArrayOutputStream byteStream; + private final String prefix; + + public LogStream() { + super(new ByteArrayOutputStream()); + this.byteStream = (ByteArrayOutputStream) this.out; + this.prefix = ""; + } + + private LogStream(String prefix) { + super(new ByteArrayOutputStream()); + this.byteStream = (ByteArrayOutputStream) this.out; + this.prefix = (prefix == null) ? "" : prefix; + } + + @Override + // ever called? + public void write(byte[] b) { + this.write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) { + String msg = new String(b, off, len); + if (msg.equals(endOfLineSystem) || msg.equals(endOfLineShort)) { // this is a newline sequence only + ejectBuffer(); + } else { + byteStream.write(b, off, len); // append message to buffer + if (msg.endsWith(endOfLineSystem) || msg.endsWith(endOfLineShort)) { // line terminated by Newline + // note that this does not seem to happen ever (even with format)!? + ejectBuffer(); + } + } + } + + @Override + // ever called? + public void write(int b) { + byteStream.write(b); + } + + @Override + public void flush() { + if (byteStream.size() > 0) { + String msg = byteStream.toString(); + if (msg.endsWith(endOfLineSystem) || msg.endsWith(endOfLineShort)) + ejectBuffer(); + } + super.flush(); + } + + @Override + public void close() { + super.close(); + } + + private void ejectBuffer() { + IJ.log(prefix + byteStream.toString()); + byteStream.reset(); + } + +} diff --git a/src/ij/io/OpenDialog.java b/src/ij/io/OpenDialog.java new file mode 100644 index 0000000..33090a3 --- /dev/null +++ b/src/ij/io/OpenDialog.java @@ -0,0 +1,266 @@ +package ij.io; +import ij.*; +import ij.gui.*; +import ij.plugin.frame.Recorder; +import ij.util.Java2; +import ij.macro.Interpreter; +import java.awt.*; +import java.io.*; +import javax.swing.*; +import javax.swing.filechooser.*; + +/** This class displays a dialog window from + * which the user can select an input file. +*/ +public class OpenDialog { + + private String dir; + private String name; + private boolean recordPath; + private static String defaultDirectory; + private static Frame sharedFrame; + private String title; + private static String lastDir, lastName; + private static boolean defaultDirectorySet; + + + /** Displays a file open dialog with 'title' as the title. */ + public OpenDialog(String title) { + this(title, null); + } + + /** Displays a file open dialog with 'title' as + the title. If 'path' is non-blank, it is + used and the dialog is not displayed. Uses + and updates the ImageJ default directory. */ + public OpenDialog(String title, String path) { + String macroOptions = Macro.getOptions(); + if (macroOptions!=null && (path==null||path.equals(""))) { + path = Macro.getValue(macroOptions, title, path); + if (path==null || path.equals("")) + path = Macro.getValue(macroOptions, "path", path); + if ((path==null || path.equals("")) && title!=null && title.equals("Open As String")) + path = Macro.getValue(macroOptions, "OpenAsString", path); + path = lookupPathVariable(path); + } + if (path==null || path.equals("")) { + if (Prefs.useJFileChooser) + jOpen(title, getDefaultDirectory(), null); + else + open(title, getDefaultDirectory(), null); + if (name!=null) + setDefaultDirectory(dir); + this.title = title; + recordPath = true; + } else { + decodePath(path); + recordPath = IJ.macroRunning(); + } + IJ.register(OpenDialog.class); + } + + /** Displays a file open dialog, using the specified + default directory and file name. */ + public OpenDialog(String title, String defaultDir, String defaultName) { + String path = null; + String macroOptions = Macro.getOptions(); + if (macroOptions!=null) + path = Macro.getValue(macroOptions, title, path); + if (path!=null) + decodePath(path); + else { + if (Prefs.useJFileChooser) + jOpen(title, defaultDir, defaultName); + else + open(title, defaultDir, defaultName); + this.title = title; + recordPath = true; + } + } + + public static String lookupPathVariable(String path) { + if (path!=null && path.indexOf(".")==-1 && !((new File(path)).exists())) { + if (path.startsWith("&")) path=path.substring(1); + Interpreter interp = Interpreter.getInstance(); + String path2 = interp!=null?interp.getStringVariable(path):null; + if (path2!=null) path = path2; + } + return path; + } + + // Uses JFileChooser to display file open dialog box. + void jOpen(String title, String path, String fileName) { + Java2.setSystemLookAndFeel(); + if (EventQueue.isDispatchThread()) + jOpenDispatchThread(title, path, fileName); + else + jOpenInvokeAndWait(title, path, fileName); + } + + // Uses the JFileChooser class to display the dialog box. + // Assumes we are running on the event dispatch thread + void jOpenDispatchThread(String title, String path, final String fileName) { + JFileChooser fc = new JFileChooser(); + fc.setDialogTitle(title); + fc.setDragEnabled(true); + fc.setTransferHandler(new DragAndDropHandler(fc)); + File fdir = null; + if (path!=null) + fdir = new File(path); + if (fdir!=null) + fc.setCurrentDirectory(fdir); + if (fileName!=null) + fc.setSelectedFile(new File(fileName)); + int returnVal = fc.showOpenDialog(IJ.getInstance()); + if (returnVal!=JFileChooser.APPROVE_OPTION) + {Macro.abort(); return;} + File file = fc.getSelectedFile(); + if (file==null) + {Macro.abort(); return;} + name = file.getName(); + dir = fc.getCurrentDirectory().getPath()+File.separator; + } + + // Run JFileChooser on event dispatch thread to avoid deadlocks + void jOpenInvokeAndWait(final String title, final String path, final String fileName) { + final boolean isMacro = Thread.currentThread().getName().endsWith("Macro$"); + try { + EventQueue.invokeAndWait(new Runnable() { + public void run() { + JFileChooser fc = new JFileChooser(); + fc.setDialogTitle(title); + fc.setDragEnabled(true); + fc.setTransferHandler(new DragAndDropHandler(fc)); + File fdir = null; + if (path!=null) + fdir = new File(path); + if (fdir!=null) + fc.setCurrentDirectory(fdir); + if (fileName!=null) + fc.setSelectedFile(new File(fileName)); + int returnVal = fc.showOpenDialog(IJ.getInstance()); + if (returnVal!=JFileChooser.APPROVE_OPTION && isMacro) + {Interpreter.abort(); return;} + File file = fc.getSelectedFile(); + if (file==null && isMacro) + {Interpreter.abort(); return;} + name = file.getName(); + dir = fc.getCurrentDirectory().getPath()+File.separator; + } + }); + } catch (Exception e) {} + } + + // Uses the AWT FileDialog class to display the dialog box + void open(String title, String path, String fileName) { + Frame parent = IJ.getInstance(); + if (parent==null) { + if (sharedFrame==null) sharedFrame = new Frame(); + parent = sharedFrame; + } + if (IJ.isMacOSX() && IJ.isJava18()) { + ImageJ ij = IJ.getInstance(); + if (ij!=null && ij.isActive()) + parent = ij; + else + parent = null; + } + FileDialog fd = new FileDialog(parent, title); + if (path!=null) { + if (IJ.isWindows() && path.contains("/")) + path = path.replaceAll("/","\\\\"); // work around FileDialog.setDirectory() bug + fd.setDirectory(path); + } + if (fileName!=null) + fd.setFile(fileName); + fd.show(); + name = fd.getFile(); + if (name==null) { + if (IJ.isMacOSX()) + System.setProperty("apple.awt.fileDialogForDirectories", "false"); + Macro.abort(); + } else + dir = fd.getDirectory(); + } + + void decodePath(String path) { + int i = path.lastIndexOf('/'); + if (i==-1) + i = path.lastIndexOf('\\'); + if (i>0) { + dir = path.substring(0, i+1); + name = path.substring(i+1); + } else { + dir = ""; + name = path; + } + } + + /** Returns the selected directory. */ + public String getDirectory() { + lastDir = dir; + return dir; + } + + /** Returns the selected file name. */ + public String getFileName() { + if (name!=null) { + if (Recorder.record && recordPath && dir!=null) + Recorder.recordPath(title, dir+name); + lastName = name; + } + return name; + } + + /** Returns the selected file path or null if the dialog was canceled. */ + public String getPath() { + if (getFileName()==null) + return null; + else return + getDirectory() + getFileName(); + } + + /** Returns the current working directory as a string + ending in the separator character ("/" or "\"), or + an empty or null string. */ + public static String getDefaultDirectory() { + if (Prefs.commandLineMacro() && !defaultDirectorySet) + return IJ.getDir("cwd"); + if (defaultDirectory==null) + defaultDirectory = Prefs.getDefaultDirectory(); + return defaultDirectory; + } + + /** Sets the current working directory. + * @see ij.plugin.frame.Editor#setDefaultDirectory + */ + public static void setDefaultDirectory(String dir) { + dir = IJ.addSeparator(dir); + defaultDirectory = dir; + defaultDirectorySet = true; + } + + /** Returns the path to the directory that contains the last file + opened or saved, or null if a file has not been opened or saved. */ + public static String getLastDirectory() { + return lastDir; + } + + /** Sets the path to the directory containing the last file opened by the user. */ + public static void setLastDirectory(String dir) { + lastDir = dir; + } + + /** Returns the name of the last file opened by the user + using a file open or file save dialog, or using drag and drop. + Returns null if the users has not opened a file. */ + public static String getLastName() { + return lastName; + } + + /** Sets the name of the last file opened by the user. */ + public static void setLastName(String name) { + lastName = name; + } + +} diff --git a/src/ij/io/Opener.java b/src/ij/io/Opener.java new file mode 100644 index 0000000..3503c3b --- /dev/null +++ b/src/ij/io/Opener.java @@ -0,0 +1,1388 @@ +package ij.io; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.plugin.frame.*; +import ij.plugin.*; +import ij.text.TextWindow; +import ij.util.Java2; +import ij.measure.ResultsTable; +import ij.macro.Interpreter; +import ij.util.Tools; +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.net.URL; +import java.net.*; +import java.util.*; +import java.util.zip.*; +import javax.swing.*; +import javax.swing.filechooser.*; +import java.awt.event.KeyEvent; +import javax.imageio.ImageIO; +import java.lang.reflect.Method; + +/** Opens tiff (and tiff stacks), dicom, fits, pgm, jpeg, bmp or + gif images, and look-up tables, using a file open dialog or a path. + Calls HandleExtraFileTypes plugin if the file type is unrecognised. */ +public class Opener { + + public static final int UNKNOWN=0,TIFF=1,DICOM=2,FITS=3,PGM=4,JPEG=5, + GIF=6,LUT=7,BMP=8,ZIP=9,JAVA_OR_TEXT=10,ROI=11,TEXT=12,PNG=13, + TIFF_AND_DICOM=14,CUSTOM=15, AVI=16, OJJ=17, TABLE=18, RAW=19; // don't forget to also update 'types' + public static final String[] types = {"unknown","tif","dcm","fits","pgm", + "jpg","gif","lut","bmp","zip","java/txt","roi","txt","png","t&d","custom","ojj","table","raw"}; + private static String defaultDirectory = null; + private int fileType; + private boolean error; + private boolean isRGB48; + private boolean silentMode; + private String omDirectory; + private File[] omFiles; + private static boolean openUsingPlugins; + private static boolean bioformats; + private String url; + + static { + Hashtable commands = Menus.getCommands(); + bioformats = commands!=null && commands.get("Bio-Formats Importer")!=null; + } + + public Opener() { + } + + /** + * Displays a file open dialog box and then opens the tiff, dicom, + * fits, pgm, jpeg, bmp, gif, lut, roi, or text file selected by + * the user. Displays an error message if the selected file is not + * in a supported format. This is the method that + * ImageJ's File/Open command uses to open files. + * @see ij.IJ#open() + * @see ij.IJ#open(String) + * @see ij.IJ#openImage() + * @see ij.IJ#openImage(String) + */ + public void open() { + OpenDialog od = new OpenDialog("Open", ""); + String directory = od.getDirectory(); + String name = od.getFileName(); + if (name!=null) { + String path = directory+name; + error = false; + open(path); + if (!error) Menus.addOpenRecentItem(path); + } + } + + /** + * Opens and displays the specified tiff, dicom, fits, pgm, jpeg, + * bmp, gif, lut, roi, or text file. Displays an error message if + * the file is not in a supported format. + * @see ij.IJ#open(String) + * @see ij.IJ#openImage(String) + */ + public void open(String path) { + boolean isURL = path.contains("://") || path.contains("file:/"); + if (isURL && isText(path)) { + openTextURL(path); + return; + } + if (path.endsWith(".jar") || path.endsWith(".class")) { + (new PluginInstaller()).install(path); + return; + } + path = makeFullPath(path); + if (!silentMode) + IJ.showStatus("Opening: " + path); + long start = System.currentTimeMillis(); + ImagePlus imp = null; + if (path.endsWith(".txt")) + this.fileType = JAVA_OR_TEXT; + else + imp = openImage(path); + if (imp==null && isURL) + return; + if (imp!=null) { + WindowManager.checkForDuplicateName = true; + if (isRGB48) + openRGB48(imp); + else + imp.show(getLoadRate(start,imp)); + } else { + switch (this.fileType) { + case LUT: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.LutLoader", path); + if (imp.getWidth()!=0) + imp.show(); + break; + case ROI: + IJ.runPlugIn("ij.plugin.RoiReader", path); + break; + case JAVA_OR_TEXT: case TEXT: + if (IJ.altKeyDown()) { // open in TextWindow if alt key down + new TextWindow(path,400,450); + IJ.setKeyUp(KeyEvent.VK_ALT); + break; + } + File file = new File(path); + int maxSize = 250000; + long size = file.length(); + if (size>=28000) { + String osName = System.getProperty("os.name"); + if (osName.equals("Windows 95") || osName.equals("Windows 98") || osName.equals("Windows Me")) + maxSize = 60000; + } + if (size64) + path = (new File(path)).getName(); + if (path.length()<=64) + msg += " \n"+path; + } + if (openUsingPlugins && msg.length()>20) + msg += "\n \nNOTE: The \"OpenUsingPlugins\" option is set."; + IJ.wait(IJ.isMacro()?500:100); // work around for OS X thread deadlock problem + IJ.error("Opener", msg); + error = true; + break; + } + } + } + + /** Displays a JFileChooser and then opens the tiff, dicom, + fits, pgm, jpeg, bmp, gif, lut, roi, or text files selected by + the user. Displays error messages if one or more of the selected + files is not in one of the supported formats. This is the method + that ImageJ's File/Open command uses to open files if + "Open/Save Using JFileChooser" is checked in EditOptions/Misc. */ + public void openMultiple() { + Java2.setSystemLookAndFeel(); + // run JFileChooser in a separate thread to avoid possible thread deadlocks + try { + EventQueue.invokeAndWait(new Runnable() { + public void run() { + JFileChooser fc = new JFileChooser(); + fc.setMultiSelectionEnabled(true); + fc.setDragEnabled(true); + fc.setTransferHandler(new DragAndDropHandler(fc)); + File dir = null; + String sdir = OpenDialog.getDefaultDirectory(); + if (sdir!=null) + dir = new File(sdir); + if (dir!=null) + fc.setCurrentDirectory(dir); + if (IJ.debugMode) IJ.log("Opener.openMultiple: "+sdir+" "+dir); + int returnVal = fc.showOpenDialog(IJ.getInstance()); + if (returnVal!=JFileChooser.APPROVE_OPTION) + return; + omFiles = fc.getSelectedFiles(); + if (omFiles.length==0) { // getSelectedFiles does not work on some JVMs + omFiles = new File[1]; + omFiles[0] = fc.getSelectedFile(); + } + omDirectory = fc.getCurrentDirectory().getPath()+File.separator; + } + }); + } catch (Exception e) {} + if (omDirectory==null) return; + OpenDialog.setDefaultDirectory(omDirectory); + for (int i=0; i6) + return true; // no extension + else + return false; + } + + /** Opens the specified file and adds it to the File/Open Recent menu. + Returns true if the file was opened successfully. */ + public boolean openAndAddToRecent(String path) { + open(path); + if (!error) + Menus.addOpenRecentItem(path); + return error; + } + + /** + * Attempts to open the specified file as a tiff, bmp, dicom, fits, + * pgm, gif or jpeg image. Returns an ImagePlus object if successful. + * Modified by Gregory Jefferis to call HandleExtraFileTypes plugin if + * the file type is unrecognised. + * @see ij.IJ#openImage(String) + */ + public ImagePlus openImage(String directory, String name) { + ImagePlus imp; + FileOpener.setSilentMode(silentMode); + if (directory.length()>0 && !(directory.endsWith("/")||directory.endsWith("\\"))) + directory += Prefs.separator; + OpenDialog.setLastDirectory(directory); + OpenDialog.setLastName(name); + String path = directory+name; + this.fileType = getFileType(path); + if (IJ.debugMode) IJ.log("openImage: \""+types[this.fileType]+"\", "+path); + switch (this.fileType) { + case TIFF: + imp = openTiff(directory, name); + return imp; + case DICOM: case TIFF_AND_DICOM: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.DICOM", path); + if (imp.getWidth()!=0) return imp; else return null; + case FITS: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.FITS_Reader", path); + if (imp.getWidth()!=0) return imp; else return null; + case PGM: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.PGM_Reader", path); + if (imp.getWidth()!=0) { + if (imp.getStackSize()==3 && imp.getBitDepth()==16) + imp = new CompositeImage(imp, IJ.COMPOSITE); + return imp; + } else + return null; + case JPEG: + imp = openJpegOrGif(directory, name); + if (imp!=null&&imp.getWidth()!=0) return imp; else return null; + case GIF: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.GIF_Reader", path); + if (imp!=null&&imp.getWidth()!=0) return imp; else return null; + case PNG: + imp = openUsingImageIO(directory+name); + if (imp!=null&&imp.getWidth()!=0) return imp; else return null; + case BMP: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.BMP_Reader", path); + if (imp.getWidth()!=0) return imp; else return null; + case ZIP: + return openZip(path); + case AVI: + AVI_Reader reader = new AVI_Reader(); + reader.setVirtual(true); + reader.displayDialog(!IJ.macroRunning()); + reader.run(path); + return reader.getImagePlus(); + case JAVA_OR_TEXT: + if (name.endsWith(".txt")) + return openTextImage(directory,name); + else + return null; + case UNKNOWN: case TEXT: + return openUsingHandleExtraFileTypes(path); + default: + return null; + } + } + + public ImagePlus openTempImage(String directory, String name) { + ImagePlus imp = openImage(directory, name); + if (imp!=null) + imp.setTemporary(); + return imp; + } + + // Call HandleExtraFileTypes plugin to see if it can handle unknown formats + // or files in TIFF format that the built in reader is unable to open. + private ImagePlus openUsingHandleExtraFileTypes(String path) { + File f = new File(path); + if (!f.exists()) + return null; + int[] wrap = new int[] {this.fileType}; + ImagePlus imp = openWithHandleExtraFileTypes(path, wrap); + if (imp!=null && imp.getNChannels()>1) + imp = new CompositeImage(imp, IJ.COLOR); + this.fileType = wrap[0]; + return imp; + } + + String getPath() { + OpenDialog od = new OpenDialog("Open", ""); + String dir = od.getDirectory(); + String name = od.getFileName(); + if (name==null) + return null; + else + return dir+name; + } + + /** Opens the specified text file as a float image. */ + public ImagePlus openTextImage(String dir, String name) { + String path = dir+name; + TextReader tr = new TextReader(); + ImageProcessor ip = tr.open(path); + return ip!=null?new ImagePlus(name,ip):null; + } + + /** + * Attempts to open the specified url as a tiff, zip compressed tiff, + * dicom, gif or jpeg. Tiff file names must end in ".tif", ZIP file names + * must end in ".zip" and dicom file names must end in ".dcm". Returns an + * ImagePlus object if successful. + * @see ij.IJ#openImage(String) + */ + public ImagePlus openURL(String url) { + url = updateUrl(url); + if (IJ.debugMode) IJ.log("OpenURL: "+url); + ImagePlus imp = openCachedImage(url); + if (imp!=null) + return imp; + try { + String name = ""; + int index = url.lastIndexOf('/'); + if (index==-1) + index = url.lastIndexOf('\\'); + if (index>0) + name = url.substring(index+1); + else + throw new MalformedURLException("Invalid URL: "+url); + if (url.indexOf(" ")!=-1) + url = url.replaceAll(" ", "%20"); + URL u = new URL(url); + IJ.showStatus(""+url); + String lurl = url.toLowerCase(Locale.US); + if (lurl.endsWith(".tif")) { + this.url = url; + imp = openTiff(u.openStream(), name); + } else if (lurl.endsWith(".zip")) + imp = openZipUsingUrl(u); + else if (lurl.endsWith(".jpg") || lurl.endsWith(".jpeg") || lurl.endsWith(".gif")) + imp = openJpegOrGifUsingURL(name, u); + else if (lurl.endsWith(".dcm") || lurl.endsWith(".ima")) { + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.DICOM", url); + if (imp!=null && imp.getWidth()==0) imp = null; + } else if (lurl.endsWith(".png")) + imp = openPngUsingURL(name, u); + else { + URLConnection uc = u.openConnection(); + String type = uc.getContentType(); + if (type!=null && (type.equals("image/jpeg")||type.equals("image/gif"))) + imp = openJpegOrGifUsingURL(name, u); + else if (type!=null && type.equals("image/png")) + imp = openPngUsingURL(name, u); + else + imp = openWithHandleExtraFileTypes(url, new int[]{0}); + } + IJ.showStatus(""); + return imp; + } catch (Exception e) { + String msg = e.getMessage(); + if (msg==null || msg.equals("")) + msg = "" + e; + msg += "\n"+url; + IJ.error("Open URL", msg); + return null; + } + } + + /** Can't open imagej.nih.gov URLs due to encryption so redirect to imagej.net mirror. */ + public static String updateUrl(String url) { + if (url==null || !url.contains("nih.gov")) + return url; + if (IJ.isJava18()) + url = url.replace("http:", "https:"); + else { + url = url.replace("imagej.nih.gov/ij", "imagej.net"); + url = url.replace("rsb.info.nih.gov/ij", "imagej.net"); + url = url.replace("rsbweb.nih.gov/ij", "imagej.net"); + } + return url; + } + + private ImagePlus openCachedImage(String url) { + if (url==null || !url.contains("/images")) + return null; + String ijDir = IJ.getDirectory("imagej"); + if (ijDir==null) + return null; + int slash = url.lastIndexOf('/'); + File file = new File(ijDir + "samples", url.substring(slash+1)); + if (!file.exists()) + return null; + if (url.endsWith(".gif")) // ij.plugin.GIF_Reader does not correctly handle inverting LUTs + return openJpegOrGif(file.getParent()+File.separator, file.getName()); + return IJ.openImage(file.getPath()); + } + + /** Used by open() and IJ.open() to open text URLs. */ + void openTextURL(String url) { + if (url.endsWith(".pdf")||url.endsWith(".zip")) + return; + String text = IJ.openUrlAsString(url); + if (text!=null && text.startsWith("