001package ball.util; 002/*- 003 * ########################################################################## 004 * Utilities 005 * $Id: CoordinateMap.java 5285 2020-02-05 04:23:21Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-util/trunk/src/main/java/ball/util/CoordinateMap.java $ 007 * %% 008 * Copyright (C) 2008 - 2020 Allen D. Ball 009 * %% 010 * Licensed under the Apache License, Version 2.0 (the "License"); 011 * you may not use this file except in compliance with the License. 012 * You may obtain a copy of the License at 013 * 014 * http://www.apache.org/licenses/LICENSE-2.0 015 * 016 * Unless required by applicable law or agreed to in writing, software 017 * distributed under the License is distributed on an "AS IS" BASIS, 018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 019 * See the License for the specific language governing permissions and 020 * limitations under the License. 021 * ########################################################################## 022 */ 023import java.lang.reflect.ParameterizedType; 024import java.lang.reflect.Type; 025import java.util.AbstractList; 026import java.util.ArrayList; 027import java.util.Collection; 028import java.util.Comparator; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032import java.util.SortedMap; 033import java.util.TreeMap; 034import javax.swing.event.EventListenerList; 035import javax.swing.event.TableModelEvent; 036import javax.swing.event.TableModelListener; 037import javax.swing.table.TableModel; 038 039/** 040 * {@link Coordinate} {@link java.util.Map} implementation. 041 * 042 * @param <V> The value type. 043 * 044 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 045 * @version $Revision: 5285 $ 046 */ 047public class CoordinateMap<V> extends MapView<Coordinate,V> 048 implements SortedMap<Coordinate,V>, TableModel { 049 private static final long serialVersionUID = -7283339807212986103L; 050 051 /** @serial */ private Coordinate min = null; 052 /** @serial */ private Coordinate max = null; 053 /** @serial */ private final EventListenerList list = new EventListenerList(); 054 055 /** 056 * Constructor to create an empty {@link CoordinateMap}. 057 */ 058 public CoordinateMap() { super(new TreeMap<>()); } 059 060 /** 061 * Constructor to specify minimum and maximum {@code Y} and {@code X}. 062 * 063 * @param y0 {@code MIN(y)} 064 * @param x0 {@code MIN(x)} 065 * @param yN {@code MAX(y) + 1} 066 * @param xN {@code MAX(x) + 1} 067 */ 068 public CoordinateMap(Number y0, Number x0, Number yN, Number xN) { 069 this(); 070 071 resize(y0, x0, yN, xN); 072 } 073 074 /** 075 * Constructor to specify maximum {@code Y} and {@code X} (origin 076 * {@code (0, 0)}). 077 * 078 * @param yN {@code MAX(y) + 1} 079 * @param xN {@code MAX(x) + 1} 080 */ 081 public CoordinateMap(Number yN, Number xN) { this(0, 0, yN, xN); } 082 083 private CoordinateMap(Map<Coordinate,V> map, 084 Number y0, Number x0, Number yN, Number xN) { 085 super(map); 086 087 resize(y0, x0, yN, xN); 088 } 089 090 @Override 091 protected SortedMap<Coordinate,V> map() { 092 return (SortedMap<Coordinate,V>) super.map(); 093 } 094 095 /** 096 * Method to get the value type of {@link.this} {@link CoordinateMap}. 097 * 098 * @return The value type {@link Class}. 099 */ 100 protected Type getType() { 101 Type type = 102 ((ParameterizedType) getClass().getGenericSuperclass()) 103 .getActualTypeArguments()[0]; 104 105 return type; 106 } 107 108 /** 109 * Method to specify new limits for the {@link CoordinateMap}. 110 * 111 * @param y0 {@code MIN(y)} 112 * @param x0 {@code MIN(x)} 113 * @param yN {@code MAX(y) + 1} 114 * @param xN {@code MAX(x) + 1} 115 * 116 * @return {@link.this} {@link CoordinateMap}. 117 */ 118 public CoordinateMap<V> resize(Number y0, Number x0, 119 Number yN, Number xN) { 120 resize(y0.intValue(), x0.intValue(), yN.intValue(), xN.intValue()); 121 122 return this; 123 } 124 125 /** 126 * Method to specify new limits for the {@link CoordinateMap} with 127 * {@code [y0, x0] = [0, 0]}. 128 * 129 * @param yN {@code MAX(y) + 1} 130 * @param xN {@code MAX(x) + 1} 131 * 132 * @return {@link.this} {@link CoordinateMap}. 133 */ 134 public CoordinateMap<V> resize(Number yN, Number xN) { 135 return resize(0, 0, yN, xN); 136 } 137 138 private void resize(int y0, int x0, int yN, int xN) { 139 min = new Coordinate(Math.min(y0, yN), Math.min(x0, xN)); 140 max = new Coordinate(Math.max(y0, yN), Math.max(x0, xN)); 141 142 keySet().retainAll(Coordinate.range(min, max)); 143 144 fireTableStructureChanged(); 145 } 146 147 public Coordinate getMin() { return min; } 148 public Coordinate getMax() { return max; } 149 150 public int getMinY() { return getMin().getY(); } 151 public int getMinX() { return getMin().getX(); } 152 153 public int getMaxY() { return getMax().getY(); } 154 public int getMaxX() { return getMax().getX(); } 155 156 /** 157 * Method to determine if the {@link Coordinate} is included in 158 * {@link.this} {@link CoordinateMap}'s space. Because the map is 159 * sparse, this method may return {@code true} when 160 * {@link #containsKey(Object)} returns {@code false}. 161 * 162 * @param coordinate The {@link Coordinate}. 163 * 164 * @return {@code true} if within the space; {@code false} otherwise. 165 */ 166 public boolean includes(Coordinate coordinate) { 167 return coordinate.within(getMin(), getMax()); 168 } 169 170 /** 171 * Method to get a {@link List} of columns 172 * (see {@link #column(Number)}). 173 * 174 * @return The {@link List} of columns. 175 */ 176 public List<CoordinateMap<V>> columns() { 177 ArrayList<CoordinateMap<V>> list = new ArrayList<>(getColumnCount()); 178 179 if (getColumnCount() > 0) { 180 for (int x = getMinX(), xN = getMaxX(); x < xN; x += 1) { 181 list.add(column(x)); 182 } 183 } 184 185 return list; 186 } 187 188 /** 189 * Method to get a {@link List} of rows (see {@link #row(Number)}). 190 * 191 * @return The {@link List} of rows. 192 */ 193 public List<CoordinateMap<V>> rows() { 194 ArrayList<CoordinateMap<V>> list = new ArrayList<>(getRowCount()); 195 196 if (getRowCount() > 0) { 197 for (int y = getMinY(), yN = getMaxY(); y < yN; y += 1) { 198 list.add(row(y)); 199 } 200 } 201 202 return list; 203 } 204 205 /** 206 * Method to get the {@link List} representing the specified column 207 * backed by the {@link CoordinateMap}. 208 * 209 * @param x The X-coordinate. 210 * 211 * @return The {@link CoordinateMap} representing the column. 212 */ 213 public CoordinateMap<V> column(Number x) { 214 return subMap(getMinY(), x, getMaxY(), x.intValue() + 1); 215 } 216 217 /** 218 * Method to get the {@link List} representing the specified row backed 219 * by the {@link CoordinateMap}. 220 * 221 * @param y The Y-coordinate. 222 * 223 * @return The {@link CoordinateMap} representing the row. 224 */ 225 public CoordinateMap<V> row(Number y) { 226 return subMap(y, getMinX(), y.intValue() + 1, getMaxX()); 227 } 228 229 /** 230 * Method to get a sub-{@link Map} of {@link.this} {@link Map} also 231 * backed by {@link.this} {@link Map}. 232 * 233 * @param y0 {@code MIN(y)} 234 * @param x0 {@code MIN(x)} 235 * @param yN {@code MAX(y) + 1} 236 * @param xN {@code MAX(x) + 1} 237 * 238 * @return The sub-{@link Map} ({@link CoordinateMap}). 239 */ 240 public CoordinateMap<V> subMap(Number y0, Number x0, 241 Number yN, Number xN) { 242 return new Sub<>(this, y0, x0, yN, xN); 243 } 244 245 /** 246 * Method to get {@link.this} {@link CoordinateMap} values as a 247 * {@link List}. Updates made through {@link List#set(int,Object)} will 248 * be made to {@link.this} {@link CoordinateMap}. 249 * 250 * @return The {@link List} of {@link CoordinateMap} values. 251 */ 252 public List<V> asList() { return new BackedList(); } 253 254 /** 255 * See {@link #containsKey(Object)}. 256 * 257 * @param y The Y-coordinate. 258 * @param x The X-coordinate. 259 * 260 * @return {@code true} if the {@link CoordinateMap} contains a key 261 * with the specified {@link Coordinate}; {@code false} 262 * otherwise. 263 */ 264 public boolean containsKey(Number y, Number x) { 265 return containsKey(new Coordinate(y, x)); 266 } 267 268 /** 269 * See {@link #get(Object)}. 270 * 271 * @param y The Y-coordinate. 272 * @param x The X-coordinate. 273 * 274 * @return The value at the coordinate (may be {@code null}). 275 */ 276 public V get(Number y, Number x) { return get(new Coordinate(y, x)); } 277 278 /** 279 * See {@link #put(Object,Object)}. 280 * 281 * @param y The Y-coordinate. 282 * @param x The X-coordinate. 283 * @param value The value at the coordinate. 284 * 285 * @return The previous value at the coordinate. 286 */ 287 public V put(Number y, Number x, V value) { 288 return put(new Coordinate(y, x), value); 289 } 290 291 @Override 292 public V put(Coordinate key, V value) { 293 if (min != null) { 294 min = 295 new Coordinate(Math.min(key.getY(), getMinY()), 296 Math.min(key.getX(), getMinX())); 297 } else { 298 min = key; 299 } 300 301 if (max != null) { 302 max = 303 new Coordinate(Math.max(key.getY() + 1, getMaxY()), 304 Math.max(key.getX() + 1, getMaxX())); 305 } else { 306 max = new Coordinate(key.getY() + 1, key.getX() + 1); 307 } 308 309 V old = super.put(key, value); 310 311 fireTableCellUpdated(key.getY() - getMinY(), key.getX() - getMinX()); 312 313 return old; 314 } 315 316 @Override 317 public V remove(Object key) { 318 V old = super.remove(key); 319 320 if (key instanceof Coordinate) { 321 Coordinate coordinate = (Coordinate) key; 322 323 fireTableCellUpdated(coordinate.getY() - getMinY(), 324 coordinate.getX() - getMinX()); 325 } 326 327 return old; 328 } 329 330 @Override 331 public Comparator<? super Coordinate> comparator() { 332 return map().comparator(); 333 } 334 335 @Override 336 public CoordinateMap<V> subMap(Coordinate from, Coordinate to) { 337 throw new UnsupportedOperationException(); 338 } 339 340 @Override 341 public CoordinateMap<V> headMap(Coordinate key) { 342 throw new UnsupportedOperationException(); 343 } 344 345 @Override 346 public CoordinateMap<V> tailMap(Coordinate key) { 347 throw new UnsupportedOperationException(); 348 } 349 350 @Override 351 public Coordinate firstKey() { return map().firstKey(); } 352 353 @Override 354 public Coordinate lastKey() { return map().lastKey(); } 355 356 @Override 357 public void clear() { 358 super.clear(); 359 fireTableDataChanged(); 360 } 361 362 @Override 363 public int getRowCount() { 364 return (getMax() != null) ? (getMaxY() - getMinY()) : 0; 365 } 366 367 @Override 368 public int getColumnCount() { 369 return (getMax() != null) ? (getMaxX() - getMinX()) : 0; 370 } 371 372 @Override 373 public String getColumnName(int x) { return null; } 374 375 @Override 376 @SuppressWarnings({ "unchecked" }) 377 public Class<? extends V> getColumnClass(int x) { 378 return (Class<V>) getType(); 379 } 380 381 @Override 382 public boolean isCellEditable(int y, int x) { return false; } 383 384 @Override 385 public V getValueAt(int y, int x) { 386 return get(y - getMinY(), x - getMinX()); 387 } 388 389 @Override 390 public void setValueAt(Object value, int y, int x) { 391 put(y - getMinY(), x - getMinX(), getColumnClass(x).cast(value)); 392 } 393 394 @Override 395 public void addTableModelListener(TableModelListener listener) { 396 list.add(TableModelListener.class, listener); 397 } 398 399 @Override 400 public void removeTableModelListener(TableModelListener listener) { 401 list.remove(TableModelListener.class, listener); 402 } 403 404 protected TableModelListener[] getTableModelListeners() { 405 return list.getListeners(TableModelListener.class); 406 } 407 408 protected void fireTableDataChanged() { 409 fireTableChanged(new TableModelEvent(this)); 410 } 411 412 protected void fireTableStructureChanged() { 413 fireTableChanged(new TableModelEvent(this, 414 TableModelEvent.HEADER_ROW)); 415 } 416 417 protected void fireTableRowsInserted(int start, int end) { 418 fireTableChanged(new TableModelEvent(this, start, end, 419 TableModelEvent.ALL_COLUMNS, 420 TableModelEvent.INSERT)); 421 } 422 423 protected void fireTableRowsUpdated(int start, int end) { 424 fireTableChanged(new TableModelEvent(this, start, end, 425 TableModelEvent.ALL_COLUMNS, 426 TableModelEvent.UPDATE)); 427 } 428 429 protected void fireTableRowsDeleted(int start, int end) { 430 fireTableChanged(new TableModelEvent(this, start, end, 431 TableModelEvent.ALL_COLUMNS, 432 TableModelEvent.DELETE)); 433 } 434 435 protected void fireTableCellUpdated(int row, int column) { 436 fireTableChanged(new TableModelEvent(this, row, row, column)); 437 } 438 439 protected void fireTableChanged(TableModelEvent event) { 440 TableModelListener[] listeners = getTableModelListeners(); 441 442 for (int i = listeners.length - 1; i >= 0; i -= 1) { 443 listeners[i].tableChanged(event); 444 } 445 } 446 447 private static class Sub<V> extends CoordinateMap<V> { 448 private static final long serialVersionUID = -7614329296625073237L; 449 450 public Sub(CoordinateMap<V> map, 451 Number y0, Number x0, Number yN, Number xN) { 452 super(map, y0, x0, yN, xN); 453 } 454 455 @Override 456 protected CoordinateMap<V> map() { 457 return (CoordinateMap<V>) super.map(); 458 } 459 460 @Override 461 public V get(Object key) { return get((Coordinate) key); } 462 463 private V get(Coordinate key) { 464 return key.within(getMin(), getMax()) ? super.get(key) : null; 465 } 466 467 @Override 468 public V put(Coordinate key, V value) { 469 if (! key.within(getMin(), getMax())) { 470 throw new IllegalArgumentException(key + " is outside " 471 + getMin() + " and " 472 + getMax()); 473 } 474 475 return super.put(key, value); 476 } 477 478 @Override 479 public V remove(Object key) { return remove((Coordinate) key); } 480 481 private V remove(Coordinate key) { 482 return key.within(getMin(), getMax()) ? super.remove(key) : null; 483 } 484 485 @Override 486 public Set<Entry<Coordinate,V>> entrySet() { 487 entrySet.clear(); 488 489 for (Entry<Coordinate,V> entry : map().entrySet()) { 490 if (entry.getKey().within(getMin(), getMax())) { 491 entrySet.add(entry); 492 } 493 } 494 495 return entrySet; 496 } 497 } 498 499 private class BackedList extends AbstractList<V> { 500 private ArrayList<Coordinate> list = new ArrayList<>(); 501 502 public BackedList() { 503 super(); 504 505 list.addAll(Coordinate.range(CoordinateMap.this.getMin(), 506 CoordinateMap.this.getMax())); 507 } 508 509 @Override 510 public int size() { return list.size(); } 511 512 @Override 513 public V get(int index) { 514 return CoordinateMap.this.get(list.get(index)); 515 } 516 517 @Override 518 public V set(int index, V value) { 519 return CoordinateMap.this.put(list.get(index), value); 520 } 521 522 @Override 523 public void clear() { throw new UnsupportedOperationException(); } 524 } 525}