/* Copyright 2013 The jeo project. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.jeo.data; import com.vividsolutions.jts.geom.Geometry; import io.jeo.geom.Bounds; import io.jeo.util.Consumer; import io.jeo.util.Function; import io.jeo.util.Optional; import io.jeo.util.Predicate; import io.jeo.vector.Feature; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Objects; /** * An iterator like object used to read contents of {@link Dataset} objects. * <p> * Example usage: * <pre> * Cursor<Object> c = getCursor() * try { * while(c.hasNext()) { * Object next = c.next(); * doSomethingWith(next); * } * } * finally { * c.close(); * } * </pre> * </p> * <p> * Since Cursor implements {@link Iterable} it can be used with for-each loops: * <pre> * for (Object next : getCursor()) { * doSomethingWith(next); * } * </pre> * However unless the cursor supports being {@link #rewind()} it is a one-time pass through the data so can only be used * for a single loop. * </p> * <p> * When used in for-each form the {@link #close()} method will be automatically called when the iterator has been * exhausted. However if the loop exists prematurely it will not be so to be safe the loop should be wrapped in a try * finally close block. * </p> * */ public abstract class Cursor<T> implements Closeable, Iterable<T> { /** * Returns <true> if the cursor has more elements. */ public abstract boolean hasNext() throws IOException; /** * Returns the next object in the cursor. * <p> * Application code should always check {@link #hasNext()} before calling this method. * </p> * <p> * Implementations should safely return <code>null</code> from this method when the cursor * has been exhausted. * </p> */ public abstract T next() throws IOException; /** * Returns an iterator view of the cursor. * <p> * This iterator implementation closes the cursor upon exhaustion. * </p> */ @Override public Iterator<T> iterator() { final Cursor<T> c = this; return new Iterator<T>() { boolean closed = false; @Override public boolean hasNext() { if (closed) { return false; } try { boolean hasNext = c.hasNext(); if (!hasNext) { //close the cursor c.close(); } return hasNext; } catch (IOException e) { throw new RuntimeException(e); } } @Override public T next() { try { return c.next(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void remove() { throw new UnsupportedOperationException(); } }; } /** * Consumes all the values of the stream. */ public void each(Consumer<T> consumer) throws IOException { try (Cursor<T> c = this) { while (c.hasNext()) { consumer.accept(c.next()); } } } /** * Returns the next element of the stream, returning empty if the stream has no more objects. */ public Optional<T> first() throws IOException { if (hasNext()) { return Optional.of(next()); } return Optional.empty(); } /** * Returns the number of results in the stream. */ public long count() throws IOException { long count = 0; try (Cursor<T> c = this) { while (c.hasNext()) { count++; c.next(); } } return count; } /** * Returns the aggregated spatial extent of results in the stream. * <p> * This method works on cursors containing {@link Feature} or {@link Geometry} objects. * Use {@link #bounds(Function)} to provide an explicit bounds provider. * </p> * * TODO: move this to FeatureStream */ public Bounds bounds() throws IOException { return bounds(new Function<T, Bounds>() { @Override public Bounds apply(T obj) { Geometry g = null; if (obj instanceof Geometry) { g = (Geometry) obj; } else if (obj instanceof Feature) { g = ((Feature) obj).geometry(); } if (g != null) { return new Bounds(g.getEnvelopeInternal()); } return null; } }); } /** * Returns the aggregated spatial extent of results in the stream. * * @param env Function providing envelopes for objects in the stream. */ public Bounds bounds(Function<T, Bounds> env) throws IOException { try (Cursor<T> c = this) { Bounds extent = new Bounds(); extent.setToNull(); while (c.hasNext()) { Bounds e = env.apply(c.next()); if (!Bounds.isNull(e)) { extent.expandToInclude(e); } } return extent; } } /** * Limits the number of results given back by the stream to a fixed size. * * @param limit The maximum number of objects to return. * * @return The limited cursor. */ public Cursor<T> limit(Integer limit) { return new LimitCursor<>(this, limit); } static class LimitCursor<T> extends CursorWrapper<T,T> { Integer limit; Integer count; LimitCursor(Cursor<T> delegate, Integer limit) { super(delegate); this.limit = Objects.requireNonNull(limit, "limit must not be null"); this.count = 0; } @Override public boolean hasNext() throws IOException { if (count < limit) { return delegate.hasNext(); } return false; } @Override public T next() throws IOException { count++; return delegate.next(); } } /** * Returns a stream that will skip over the specified number of objects. * * @param offset The number of objects to skip over. * * @return The skipped cursor. */ public Cursor<T> skip(Integer offset) { return new OffsetCursor<>(this, offset); } static class OffsetCursor<T> extends CursorWrapper<T,T> { Integer offset; OffsetCursor(Cursor<T> delegate, Integer offset) { super(delegate); this.offset = Objects.requireNonNull(offset, "offset must not be null"); } @Override public boolean hasNext() throws IOException { if (offset != null) { for (int i = 0; i < offset && delegate.hasNext(); i++) { delegate.next(); } offset = null; } return delegate.hasNext(); } } /** * Wraps a stream returning objects that pass a predicate. * * @param filter The predicate used to filter objects. * * @return The filtered cursor. */ public Cursor<T> filter(Predicate<T> filter) { return new FilterCursor<>(this, filter); } private static class FilterCursor<T> extends CursorWrapper<T,T> { Predicate<T> filter; T next; FilterCursor(Cursor<T> delegate, Predicate<T> filter) { super(delegate); this.filter = Objects.requireNonNull(filter, "filter must not be null"); } @Override public boolean hasNext() throws IOException { while(delegate.hasNext() && next == null) { T obj = delegate.next(); if (filter.test(obj)) { next = obj; } } return next != null; } @Override public T next() throws IOException { T obj = next; next = null; return obj; } } /** * Returns a cursor capable of being {@link #rewind()} within a certain number * of objects. * <p> * The <tt>n</tt> parameter specifies the buffer size. Up to n objects can be * read from the cursor before calling {@link #rewind()}. After n objects have * been read the cursor stops buffering and delegates to the original cursor. * </p> * @param n The buffer size. */ public Cursor<T> buffer(int n) { return new BufferedCursor(this, n); } /** * Rewinds the cursor to it's initial state. * <p> * Whether a cursor is rewindable or not depends on the implementation. The {@link #buffer(int)} method can be * used to create a rewindable cursor. * </p> * <p> */ public void rewind() { throw new UnsupportedOperationException("cursor not rewindable"); } static class BufferedCursor<T> extends CursorWrapper<T,T> { int bufferSize; List<T> buffer; /* buffer index */ int i = 0; /* total read */ int total = 0; BufferedCursor(Cursor delegate, int bufferSize) { super(delegate); this.bufferSize = Objects.requireNonNull(bufferSize, "bufferSize must not be null"); this.buffer = new ArrayList<>(bufferSize); } @Override public boolean hasNext() throws IOException { if (i < buffer.size()) { return true; } return delegate.hasNext(); } @Override public T next() throws IOException { if (i < buffer.size()) { return buffer.get(i++); } T next = delegate.next(); if (buffer.size() < bufferSize) { buffer.add(next); i++; } total++; return next; } @Override public void rewind() { if (total > bufferSize) { // past the buffer, can't rewind delegate.rewind(); } i = total = 0; } } /** * Applies a mapping function to the stream. * * @param mapper the mapping function. * * @return The new cursor. */ public <R> Cursor<R> map(final Function<T,R> mapper) { Objects.requireNonNull(mapper, "mapper must not be null"); return new CursorWrapper<T,R>(this) { @Override public R next() throws IOException { return mapper.apply(delegate.next()); } }; } protected static class CursorWrapper<T,R> extends Cursor<R> { protected Cursor<T> delegate; protected CursorWrapper(Cursor<T> delegate) { this.delegate = delegate; } @Override public boolean hasNext() throws IOException { return delegate.hasNext(); } @Override public R next() throws IOException { return (R) delegate.next(); } @Override public void rewind() { delegate.rewind(); } @Override public void close() throws IOException { delegate.close(); } } }