// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.datatype; import java.awt.Color; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferInt; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.Arrays; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import org.infinity.gui.RenderCanvas; import org.infinity.gui.StructViewer; import org.infinity.gui.ViewerUtil; import org.infinity.icon.Icons; import org.infinity.resource.AbstractStruct; import org.infinity.resource.StructEntry; import org.infinity.resource.graphics.ColorConvert; /** Implements a RGB color picker control. */ public class ColorPicker extends Datatype implements Editable, IsNumeric, MouseListener, FocusListener { /** Supported color formats. */ public enum Format { /** Byte order: {unused, red, green, blue} */ XRGB, /** Byte order: {red, green, blue, unused} */ RGBX, /** Byte order: {blue, green red, unused} */ BGRX, /** Byte order: {unused blue, green red} */ XBGR, } private final int shiftRed, shiftGreen, shiftBlue; private RenderCanvas rcMainPreview, rcSecondPreview, rcColorPreview; private JTextField tfHue, tfSat, tfBri, tfRed, tfGreen, tfBlue; private int tmpHue, tmpSat, tmpBri, tmpRed, tmpGreen, tmpBlue; private int value; /** Initializing color picker with the most commonly used color format {@code Format.XRGB}. */ public ColorPicker(ByteBuffer buffer, int offset, String name) { this(null, buffer, offset, name, Format.XRGB); } /** Initializing color picker with the most commonly used color format {@code Format.XRGB}. */ public ColorPicker(StructEntry parent, ByteBuffer buffer, int offset, String name) { this(parent, buffer, offset, name, Format.XRGB); } public ColorPicker(ByteBuffer buffer, int offset, String name, Format fmt) { this(null, buffer, offset, name, fmt); } public ColorPicker(StructEntry parent, ByteBuffer buffer, int offset, String name, Format fmt) { super(parent, offset, 4, name); switch (fmt) { case RGBX: shiftRed = 0; shiftGreen = 8; shiftBlue = 16; break; case XRGB: shiftRed = 8; shiftGreen = 16; shiftBlue = 24; break; case BGRX: shiftRed = 16; shiftGreen = 8; shiftBlue = 0; break; case XBGR: shiftRed = 24; shiftGreen = 16; shiftBlue = 8; break; default: shiftRed = shiftGreen = shiftBlue = 0; break; } read(buffer, offset); } //--------------------- Begin Interface Editable --------------------- @Override public JComponent edit(ActionListener container) { rcMainPreview = new RenderCanvas(); // use H as x and B as y rcMainPreview.setSize(256, 128); rcMainPreview.setPreferredSize(rcMainPreview.getSize()); rcMainPreview.setBorder(BorderFactory.createLineBorder(Color.BLACK)); rcMainPreview.addMouseListener(this); rcMainPreview.setImage(ColorConvert.createCompatibleImage(rcMainPreview.getWidth(), rcMainPreview.getHeight(), false)); rcSecondPreview = new RenderCanvas(); // use S rcSecondPreview.setSize(32, 128); rcSecondPreview.setPreferredSize(rcSecondPreview.getSize()); rcSecondPreview.setBorder(BorderFactory.createLineBorder(Color.BLACK)); rcSecondPreview.addMouseListener(this); rcSecondPreview.setImage(ColorConvert.createCompatibleImage(rcSecondPreview.getWidth(), rcSecondPreview.getHeight(), false)); JLabel lPreview = new JLabel("Preview"); rcColorPreview = new RenderCanvas(); // shows currently defined color rcColorPreview.setSize(64, 32); rcColorPreview.setPreferredSize(rcColorPreview.getSize()); rcColorPreview.setBorder(BorderFactory.createLineBorder(Color.BLACK)); rcColorPreview.setBackground(Color.BLACK); rcColorPreview.setImage(ColorConvert.createCompatibleImage(rcColorPreview.getWidth(), rcColorPreview.getHeight(), false)); JLabel lHue = new JLabel("H:"); JLabel lSat = new JLabel("S:"); JLabel lBri = new JLabel("B:"); tfHue = new JTextField(4); // range: [0..359] tfHue.addFocusListener(this); tfSat = new JTextField(4); // range: [0..100] tfSat.addFocusListener(this); tfBri = new JTextField(4); // range: [0..100] tfBri.addFocusListener(this); JLabel lHue2 = new JLabel("\u00B0"); JLabel lSat2 = new JLabel("%"); JLabel lBri2 = new JLabel("%"); JLabel lR = new JLabel("R:"); JLabel lG = new JLabel("G:"); JLabel lB = new JLabel("B:"); tfRed = new JTextField(4); // range: [0..255] tfRed.addFocusListener(this); tfGreen = new JTextField(4); // range: [0..255] tfGreen.addFocusListener(this); tfBlue = new JTextField(4); // range: [0..255] tfBlue.addFocusListener(this); JButton bUpdate = new JButton("Update value", Icons.getIcon(Icons.ICON_REFRESH_16)); bUpdate.addActionListener(container); bUpdate.setActionCommand(StructViewer.UPDATE_VALUE); GridBagConstraints gbc = new GridBagConstraints(); // Setting up HSB controls JPanel pHSB = new JPanel(new GridBagLayout()); gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0); pHSB.add(lHue, gbc); gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 0), 0, 0); pHSB.add(tfHue, gbc); gbc = ViewerUtil.setGBC(gbc, 2, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 0), 0, 0); pHSB.add(lHue2, gbc); gbc = ViewerUtil.setGBC(gbc, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(2, 0, 0, 0), 0, 0); pHSB.add(lSat, gbc); gbc = ViewerUtil.setGBC(gbc, 1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(2, 4, 0, 0), 0, 0); pHSB.add(tfSat, gbc); gbc = ViewerUtil.setGBC(gbc, 2, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(2, 4, 0, 0), 0, 0); pHSB.add(lSat2, gbc); gbc = ViewerUtil.setGBC(gbc, 0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(2, 0, 0, 0), 0, 0); pHSB.add(lBri, gbc); gbc = ViewerUtil.setGBC(gbc, 1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(2, 4, 0, 0), 0, 0); pHSB.add(tfBri, gbc); gbc = ViewerUtil.setGBC(gbc, 2, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(2, 4, 0, 0), 0, 0); pHSB.add(lBri2, gbc); // Setting up RGB controls JPanel pRGB = new JPanel(new GridBagLayout()); gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0); pRGB.add(lR, gbc); gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 0), 0, 0); pRGB.add(tfRed, gbc); gbc = ViewerUtil.setGBC(gbc, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(2, 0, 0, 0), 0, 0); pRGB.add(lG, gbc); gbc = ViewerUtil.setGBC(gbc, 1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(2, 4, 0, 0), 0, 0); pRGB.add(tfGreen, gbc); gbc = ViewerUtil.setGBC(gbc, 0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(2, 0, 0, 0), 0, 0); pRGB.add(lB, gbc); gbc = ViewerUtil.setGBC(gbc, 1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(2, 4, 0, 0), 0, 0); pRGB.add(tfBlue, gbc); // Setting up color preview JPanel pPreview = new JPanel(new GridBagLayout()); gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0); pPreview.add(lPreview, gbc); gbc = ViewerUtil.setGBC(gbc, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0); pPreview.add(rcColorPreview, gbc); // Setting up main controls JPanel pControls = new JPanel(new GridBagLayout()); gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0); pControls.add(pHSB, gbc); gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 16, 0, 0), 0, 0); pControls.add(pRGB, gbc); gbc = ViewerUtil.setGBC(gbc, 0, 1, 2, 1, 1.0, 1.0, GridBagConstraints.LAST_LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0); pControls.add(new JPanel(), gbc); gbc = ViewerUtil.setGBC(gbc, 0, 2, 2, 1, 1.0, 0.0, GridBagConstraints.LAST_LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0); pControls.add(pPreview, gbc); JPanel pMain = new JPanel(new GridBagLayout()); pMain.setBorder(BorderFactory.createEtchedBorder()); gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(8, 8, 8, 0), 0, 0); pMain.add(rcMainPreview, gbc); gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(8, 8, 8, 0), 0, 0); pMain.add(rcSecondPreview, gbc); gbc = ViewerUtil.setGBC(gbc, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(8, 16, 8, 8), 0, 0); pMain.add(pControls, gbc); // Setting up main panel JPanel panel = new JPanel(new GridBagLayout()); gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0); panel.add(pMain, gbc); gbc = ViewerUtil.setGBC(gbc, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(4, 0, 0, 0), 0, 0); panel.add(bUpdate, gbc); panel.setMinimumSize(panel.getPreferredSize()); updateColorValues(value); return panel; } @Override public void select() { updateColorValues(value); updateColor(); } @Override public boolean updateValue(AbstractStruct struct) { value = getInputRgbValue(); // notifying listeners fireValueUpdated(new UpdateEvent(this, struct)); return true; } //--------------------- End Interface Editable --------------------- //--------------------- Begin Interface Writeable --------------------- @Override public void write(OutputStream os) throws IOException { writeInt(os, value); } //--------------------- End Interface Writeable --------------------- //--------------------- Begin Interface Readable --------------------- @Override public int read(ByteBuffer buffer, int offset) { buffer.position(offset); value = buffer.getInt(); tmpRed = getRed(value); tmpGreen = getGreen(value); tmpBlue = getBlue(value); float[] hsb = {0.0f, 0.0f, 0.0f}; Color.RGBtoHSB(tmpRed, tmpGreen, tmpBlue, hsb); tmpHue = (int)Math.round(hsb[0]*360.0f); tmpSat = (int)Math.round(hsb[1]*100.0f); tmpBri = (int)Math.round(hsb[2]*100.0f); return offset + getSize(); } //--------------------- End Interface Readable --------------------- //--------------------- Begin Interface MouseListener --------------------- @Override public void mouseClicked(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { if (e.getSource() == rcMainPreview && e.getButton() == MouseEvent.BUTTON1) { updateMainPreviewValue(e.getX(), e.getY()); updateRgbFromHsb(); updateColor(); } else if (e.getSource() == rcSecondPreview && e.getButton() == MouseEvent.BUTTON1) { updateSecondPreviewValue(e.getY()); updateRgbFromHsb(); updateColor(); } } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } //--------------------- End Interface MouseListener --------------------- //--------------------- Begin Interface FocusListener --------------------- @Override public void focusGained(FocusEvent e) { if (e.getSource() instanceof JTextField) { ((JTextField)e.getSource()).selectAll(); } } @Override public void focusLost(FocusEvent e) { if (e.getSource() == tfRed) { int v = validateNumberInput(tfRed, tmpRed, 0, 255); if (v != tmpRed) { tmpRed = v; updateHsbFromRgb(); updateInputHsb(); updatePreview(); } } else if (e.getSource() == tfGreen) { int v = validateNumberInput(tfGreen, tmpGreen, 0, 255); if (v != tmpGreen) { tmpGreen = v; updateHsbFromRgb(); updateInputHsb(); updatePreview(); } } else if (e.getSource() == tfBlue) { int v = validateNumberInput(tfBlue, tmpBlue, 0, 255); if (v != tmpBlue) { tmpBlue = v; updateHsbFromRgb(); updateInputHsb(); updatePreview(); } } else if (e.getSource() == tfHue) { int v = validateNumberInput(tfHue, tmpHue, 0, 360); if (v != tmpHue) { tmpHue = v; updateRgbFromHsb(); updateInputRgb(); updatePreview(); } } else if (e.getSource() == tfSat) { int v = validateNumberInput(tfSat, tmpSat, 0, 100); if (v != tmpSat) { tmpSat = v; updateRgbFromHsb(); updateInputRgb(); updatePreview(); } } else if (e.getSource() == tfBri) { int v = validateNumberInput(tfBri, tmpBri, 0, 100); if (v != tmpBri) { tmpBri = v; updateRgbFromHsb(); updateInputRgb(); updatePreview(); } } } //--------------------- End Interface FocusListener --------------------- @Override public String toString() { return String.format("Red: %1$d, Green: %2$d, Blue: %3$d", getRed(value), getGreen(value), getBlue(value)); } //--------------------- Begin Interface IsNumeric --------------------- @Override public long getLongValue() { return (long)value & 0xffffffffL; } @Override public int getValue() { return value; } //--------------------- End Interface IsNumeric --------------------- // r, g, b in range [0..255] private int getRgbValue(int r, int g, int b) { return ((r & 0xff) << shiftRed) | ((g & 0xff) << shiftGreen) | ((b & 0xff) << shiftBlue); } // h, s, b in range [0..1] private int getHsbValue(float h, float s, float b) { if (h < 0.0f) h = 0.0f; else if (h > 1.0f) h = 1.0f; if (s < 0.0f) s = 0.0f; else if (s > 1.0f) s = 1.0f; if (b < 0.0f) b = 0.0f; else if (b > 1.0f) b = 1.0f; Color c = new Color(Color.HSBtoRGB(h, s, b)); return getRgbValue(c.getRed(), c.getGreen(), c.getBlue()); } private int getRed(int color) { return (color >>> shiftRed) & 0xff; } private int getGreen(int color) { return (color >>> shiftGreen) & 0xff; } private int getBlue(int color) { return (color >>> shiftBlue) & 0xff; } // Returns a color value based on the RGB input fields private int getInputRgbValue() throws NumberFormatException { return getRgbValue(getInputRed(), getInputGreen(), getInputBlue()); } private int getInputRed() throws NumberFormatException { int v = Integer.parseInt(tfRed.getText()); if (v < 0) v = 0; else if (v > 255) v = 255; return v; } private int getInputGreen() throws NumberFormatException { int v = Integer.parseInt(tfGreen.getText()); if (v < 0) v = 0; else if (v > 255) v = 255; return v; } private int getInputBlue() throws NumberFormatException { int v = Integer.parseInt(tfBlue.getText()); if (v < 0) v = 0; else if (v > 255) v = 255; return v; } // Checks and returns either the number fetched from the input field or oldVal on error private int validateNumberInput(JTextField tf, int oldVal, int min, int max) { if (tf != null) { try { oldVal = Integer.parseInt(tf.getText()); if (oldVal < min) oldVal = min; else if (oldVal > max) oldVal = max; } catch (NumberFormatException nfe) { } tf.setText(Integer.toString(oldVal)); } return oldVal; } // Returns color value based on the main preview coordinates and the saturation value from the input field private void updateMainPreviewValue(int x, int y) { if (x < 0) x = 0; else if (x >= rcMainPreview.getWidth()) x = rcMainPreview.getWidth() - 1; if (y < 0) y = 0; else if (y >= rcMainPreview.getHeight()) y = rcMainPreview.getHeight() - 1; tmpHue = x * 360 / rcMainPreview.getWidth(); tmpBri = 100 - (y * 100 / rcMainPreview.getHeight()); } // Returns color value based on the secondary preview coordinate and the hue/brightness values from the input fields private void updateSecondPreviewValue(int y) { if (y < 0) y = 0; else if (y >= rcSecondPreview.getHeight()) y = rcSecondPreview.getHeight() - 1; tmpSat = 100 - (y * 100 / rcSecondPreview.getHeight()); } private void updateRgbFromHsb() { Color c = new Color(Color.HSBtoRGB((float)tmpHue / 360.0f, (float)tmpSat / 100.0f, (float)tmpBri / 100.0f)); tmpRed = c.getRed(); tmpGreen = c.getGreen(); tmpBlue = c.getBlue(); } private void updateHsbFromRgb() { float[] hsb = {0.0f, 0.0f, 0.0f}; Color.RGBtoHSB(tmpRed, tmpGreen, tmpBlue, hsb); tmpHue = (int)Math.round(hsb[0]*360.0f); tmpSat = (int)Math.round(hsb[1]*100.0f); tmpBri = (int)Math.round(hsb[2]*100.0f); } // Updates all temporary values for each color component private void updateColorValues(int value) { tmpRed = getRed(value); tmpGreen = getGreen(value); tmpBlue = getBlue(value); updateHsbFromRgb(); } // Update RGB input controls only private void updateInputRgb() { tfRed.setText(Integer.toString(tmpRed)); if (tfRed.hasFocus()) tfRed.selectAll(); tfGreen.setText(Integer.toString(tmpGreen)); if (tfGreen.hasFocus()) tfGreen.selectAll(); tfBlue.setText(Integer.toString(tmpBlue)); if (tfBlue.hasFocus()) tfBlue.selectAll(); } // Update HSB input controls only private void updateInputHsb() { tfHue.setText(Integer.toString(tmpHue)); if (tfHue.hasFocus()) tfHue.selectAll(); tfSat.setText(Integer.toString(tmpSat)); if (tfSat.hasFocus()) tfSat.selectAll(); tfBri.setText(Integer.toString(tmpBri)); if (tfBri.hasFocus()) tfBri.selectAll(); } // Update preview controls only private void updatePreview() { // update main preview updateMainPreview(); // update secondary preview updateSecondPreview(); // update color preview updateColorPreview(); } // Update controls with given color value private void updateColor() { updateInputRgb(); updateInputHsb(); updatePreview(); } // Update main (hue/brightness) preview private void updateMainPreview() { // drawing background initMainPreviewMap(); // drawing marker BufferedImage image = (BufferedImage)rcMainPreview.getImage(); if (image != null) { Graphics2D g = image.createGraphics(); if (g != null) { try { int x = tmpHue * image.getWidth() / 360; int y = (100 - tmpBri) * image.getHeight() / 100; g.setColor(Color.WHITE); g.drawLine(x + 2, y, x + 6, y); g.drawLine(x - 2, y, x - 6, y); g.drawLine(x, y + 2, x, y + 6); g.drawLine(x, y - 2, x, y - 6); } finally { g.dispose(); g = null; } } } rcMainPreview.repaint(); } // Update secondary (saturation) preview private void updateSecondPreview() { BufferedImage image = (BufferedImage)rcSecondPreview.getImage(); if (image != null) { int width = image.getWidth(); int height = image.getHeight(); int type = image.getRaster().getDataBuffer().getDataType(); if (type == DataBuffer.TYPE_INT) { int[] buffer = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); // drawing gradient and marker float h = (float)tmpHue / 360.0f; float b = (float)tmpBri / 100.0f; int marker = (100 - tmpSat) * height / 100; for (int y = 0; y < height; y++) { float sat = 1.0f - ((float)y / (float)height); int rgb = (y == marker) ? 0xffffff : Color.HSBtoRGB(h, sat, b); int ofs = y*width; for (int x = 0; x < width; x++, ofs++) { buffer[ofs] = rgb; } } } } rcSecondPreview.repaint(); } // Update color preview box private void updateColorPreview() { BufferedImage image = (BufferedImage)rcColorPreview.getImage(); if (image != null) { int type = image.getRaster().getDataBuffer().getDataType(); if (type == DataBuffer.TYPE_INT) { int[] buffer = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); int rgb = (tmpRed << 16) | (tmpGreen << 8) | tmpBlue; Arrays.fill(buffer, rgb); } } rcColorPreview.repaint(); } // Update main preview background map private void initMainPreviewMap() { BufferedImage image = (BufferedImage)rcMainPreview.getImage(); if (image != null) { int width = image.getWidth(); int height = image.getHeight(); int[] buffer = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); if (buffer != null) { float s = (float)tmpSat / 100.0f; for (int y = 0; y < height; y++) { float b = 1.0f - ((float)y / (float)height); for (int x = 0; x < width; x++) { float h = (float)x / (float) width; int rgb = getHsbValue(h, s, b); buffer[y*width + x] = (getRed(rgb) << 16) | (getGreen(rgb) << 8) | getBlue(rgb); } } } } } }