/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2008 - 2010, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.map;
import java.io.IOException;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EventObject;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import org.apache.sis.geometry.GeneralEnvelope;
import org.geotoolkit.referencing.CRS;
import org.geotoolkit.style.StyleConstants;
import static org.apache.sis.util.ArgumentChecks.*;
import org.apache.sis.measure.NumberRange;
import org.geotoolkit.util.collection.CollectionChangeEvent;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.util.Utilities;
/**
* The default implementation of the {@linkplain org.geotoolkit.map.MapContext}
* interface.
* Mapcontext has a tree structure which can be viewed as a MapLayer list using
* the 'layers' method.
*
* @author Johann Sorel (Geomatys)
* @module
*/
final class DefaultMapContext extends DefaultMapItem implements MapContext, LayerListener {
private final AdapterList layers = new AdapterList();
private final LayerListener.Weak layerListener = new LayerListener.Weak(this);
private CoordinateReferenceSystem crs = null;
private Envelope area = null;
public DefaultMapContext(final CoordinateReferenceSystem crs) {
ensureNonNull("crs", crs);
this.crs = crs;
desc = StyleConstants.DEFAULT_DESCRIPTION;
}
/**
* {@inheritDoc }
* This method is thread safe.
*/
@Override
public void setCoordinateReferenceSystem(final CoordinateReferenceSystem crs) {
ensureNonNull("crs", crs);
synchronized (this) {
if(Utilities.equalsIgnoreMetadata(this.crs,crs)) return;
if(this.area != null){
try {
//update the area of interest
final Envelope newEnv = Envelopes.transform(area, crs);
setAreaOfInterest(newEnv);
} catch (TransformException ex) {
LOGGER.log(Level.WARNING, null, ex);
}
}
}
}
/**
* {@inheritDoc }
* This method is thread safe.
*/
@Override
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
return crs;
}
/**
* {@inheritDoc }
*/
@Override
public List<MapLayer> layers() {
return layers;
}
/**
* Get the bounding box of all the layers in this MapContext. If all the
* layers cannot determine the bounding box in the speed required for each
* layer, then null is returned. The bounds will be expressed in the
* MapContext coordinate system.
*
* @return The bounding box of all layers.
*/
@Override
public Envelope getBounds() throws IOException {
return getBounds(false);
}
/**
* {@inheritDoc }
*
* @param onlyVisible
* @return
* @throws IOException
*/
@Override
public Envelope getBounds(boolean onlyVisible) throws IOException {
GeneralEnvelope result = null;
GeneralEnvelope env;
CoordinateReferenceSystem sourceCrs;
for(final MapLayer layer : layers){
if(onlyVisible && !layer.isVisible()) continue;
env = new GeneralEnvelope(layer.getBounds());
sourceCrs = env.getCoordinateReferenceSystem();
if (!env.isAllNaN()) {
boolean addToResult = false;
if ((sourceCrs != null) && (crs != null) && !Utilities.equalsIgnoreMetadata(sourceCrs,crs)) {
try {
env = new GeneralEnvelope(Envelopes.transform(env, crs));
addToResult = true;
} catch (TransformException e) {
LOGGER.log(Level.WARNING,
"Data source and map context coordinate system differ, yet it was not possible to get a projected bounds estimate...",
e);
}
} else {
addToResult = true;
}
if (addToResult) {
if (result == null) {
result = env;
} else {
result.add(env);
}
}
}
}
if (result == null|| result.isEmpty()) {
//we could not find a valid envelope
result = new GeneralEnvelope(CRS.getEnvelope(crs));
}
return result;
}
/**
* {@inheritDoc }
*/
@Override
public Envelope getAreaOfInterest() {
if(area != null){
return new GeneralEnvelope(area);
}else{
return null;
}
}
/**
* {@inheritDoc }
*/
@Override
public void setAreaOfInterest(final Envelope aoi) {
ensureNonNull("area of interest", aoi);
final Envelope oldEnv;
synchronized (this) {
oldEnv = this.area;
if(this.area != null && oldEnv.equals(aoi)){
return;
}
this.area = aoi;
}
firePropertyChange(AREA_OF_INTEREST_PROPERTY, oldEnv, this.area);
}
//--------------------------------------------------------------------------
// listeners management ----------------------------------------------------
//--------------------------------------------------------------------------
/**
* {@inheritDoc }
*/
@Override
public void addContextListener(final ContextListener listener){
listeners.add(ContextListener.class, listener);
addItemListener(listener);
}
/**
* {@inheritDoc }
*/
@Override
public void removeContextListener(final ContextListener listener){
listeners.remove(ContextListener.class, listener);
removeItemListener(listener);
}
protected void updateTree(final int type, final MapLayer layer, final NumberRange<Integer> range, final EventObject orig) {
//update the tree
final List<MapLayer> list = Collections.singletonList(layer);
final int startIndex = range.getMinValue();
for(int i=list.size()-1;i>=0;i--){
if(type == CollectionChangeEvent.ITEM_ADDED){
updateItemAdd(list.get(i), startIndex+i);
}else if(type == CollectionChangeEvent.ITEM_REMOVED){
updateItemRemove(startIndex+i);
}
}
}
private void updateItemAdd(final MapLayer layer, final int index){
if(index==0){
items.add(0, layer);
}else{
final MapLayer beforeElement = layers.get(index-1);
final MapItem parent = findParentForLayerNumber(this, index-1, new AtomicInteger(-1));
final int beforeIndex = parent.items().indexOf(beforeElement);
parent.items().add(beforeIndex+1, layer);
}
}
private void updateItemRemove(final int index){
findAndRemoveForLayerNumber(this, index, new AtomicInteger(-1));
}
protected void fireLayerChange(final int type, final MapLayer layer, final NumberRange<Integer> range, final EventObject orig) {
//fire the event
final CollectionChangeEvent<MapLayer> event = new CollectionChangeEvent<MapLayer>(this, layer, type, range, orig);
final ContextListener[] lists = listeners.getListeners(ContextListener.class);
for (ContextListener listener : lists) {
listener.layerChange(event);
}
}
protected void fireLayerChange(final int type, final Collection<MapLayer> layers, final NumberRange<Integer> range, final EventObject orig) {
//fire the event
final CollectionChangeEvent<MapLayer> event = new CollectionChangeEvent<MapLayer>(this, layers, type, range, orig);
final ContextListener[] lists = listeners.getListeners(ContextListener.class);
for (ContextListener listener : lists) {
listener.layerChange(event);
}
}
private MapItem findParentForLayerNumber(final MapItem root, final int wishedNumber, final AtomicInteger inc){
for(MapItem item : root.items()){
if(item instanceof MapLayer){
if(inc.incrementAndGet() == wishedNumber){
return root;
}
}else{
final MapItem test = findParentForLayerNumber(item, wishedNumber, inc);
if(test != null){
return test;
}
}
}
return null;
}
private boolean findAndRemoveForLayerNumber(final MapItem root, final int wishedNumber, final AtomicInteger inc){
for(MapItem item : root.items()){
if(item instanceof MapLayer){
if(inc.incrementAndGet() == wishedNumber){
root.items().remove(item);
return true;
}
}else{
final boolean found = findAndRemoveForLayerNumber(item, wishedNumber, inc);
if(found){
return found;
}
}
}
return false;
}
//--------------------------------------------------------------------------
// layer listener ----------------------------------------------------------
//--------------------------------------------------------------------------
/**
* In case item is a MapLayer we register it using the layer listener.
*/
@Override
protected void registerListenerSource(final MapItem item) {
if(item instanceof MapLayer){
layerListener.registerSource((MapLayer) item);
}else{
super.registerListenerSource(item);
}
}
/**
* In case item is a MapLayer we register it using the layer listener.
*/
@Override
protected void unregisterListenerSource(final MapItem item) {
if(item instanceof MapLayer){
layerListener.unregisterSource((MapLayer) item);
}else{
super.unregisterListenerSource(item);
}
}
@Override
public void styleChange(final MapLayer source, final EventObject event) {
}
//--------------------------------------------------------------------------
// item changes, update layers list ----------------------------------------
//--------------------------------------------------------------------------
@Override
protected void fireItemChange(final int type, final Collection<? extends MapItem> items, final NumberRange<Integer> range) {
super.fireItemChange(type, items, range);
final Collection<MapLayer> candidates = new ArrayList<MapLayer>();
final MapItem[] array = items.toArray(new MapItem[items.size()]);
int from = -1;
int to = -1;
for(MapItem it : array){
if(it instanceof MapLayer){
candidates.add((MapLayer) it);
final int index = layers.indexOf(it);
if(from == -1){
from = index;
to = index;
}else{
from = Math.min(index, from);
to = Math.max(index, to);
}
}
}
layers.clearCache();
if(!candidates.isEmpty()){
fireLayerChange(type, candidates, NumberRange.create(from, true, to, true), null);
}
}
@Override
protected void fireItemChange(final int type, final MapItem item, final NumberRange<Integer> range, final EventObject orig) {
super.fireItemChange(type, item, range, orig);
//forward event a MapLayer event if necessary
int index =-1;
if(item instanceof MapLayer){
index = layers.indexOf(item);
}
layers.clearCache();
if(item instanceof MapLayer){
fireLayerChange(type, (MapLayer) item, NumberRange.create(index, true, index, true), orig);
}
}
private List<MapLayer> createLayerList(final MapItem item, final List<MapLayer> buffer){
if(item instanceof MapLayer){
buffer.add((MapLayer) item);
}else{
for(MapItem child : item.items()){
createLayerList(child, buffer);
}
}
return buffer;
}
/**
* Special list wish only raise events on add/remove calls.
*/
private final class AdapterList extends AbstractList<MapLayer> {
private MapLayer[] cache = null;
private synchronized MapLayer[] getCache(){
if(cache == null){
final List<MapLayer> layers = createLayerList(DefaultMapContext.this, new ArrayList<MapLayer>());
cache = layers.toArray(new MapLayer[layers.size()]);
}
return cache;
}
private synchronized void clearCache(){
cache = null;
}
@Override
public MapLayer get(final int index) {
return getCache()[index];
}
@Override
public MapLayer set(final int index, final MapLayer element) {
final MapLayer old = remove(index);
add(index,element);
return old;
}
@Override
public void add(final int index, final MapLayer element) {
updateTree(CollectionChangeEvent.ITEM_ADDED, element, NumberRange.create(index, true, index, true), null);
}
@Override
public MapLayer remove(final int index) {
final MapLayer[] array = getCache();
updateTree(CollectionChangeEvent.ITEM_REMOVED, array[index], NumberRange.create(index, true, index, true), null);
final MapLayer removed = array[index];
return removed;
}
@Override
public int size() {
return getCache().length;
}
};
}