// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.layer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.openstreetmap.josm.gui.NavigatableComponent;
/**
* Represents a layer that has native scales.
* @author András Kolesár
* @since 9818 (creation)
* @since 10600 (functional interface)
*/
@FunctionalInterface
public interface NativeScaleLayer {
/**
* Get native scales of this layer.
* @return {@link ScaleList} of native scales
*/
ScaleList getNativeScales();
/**
* Represents a scale with native flag, used in {@link ScaleList}
*/
class Scale {
/**
* Scale factor, same unit as in {@link NavigatableComponent}
*/
private final double scale;
/**
* True if this scale is native resolution for data source.
*/
private final boolean isNative;
private final int index;
/**
* Constructs a new Scale with given scale, native defaults to true.
* @param scale as defined in WMTS (scaleDenominator)
* @param index zoom index for this scale
*/
public Scale(double scale, int index) {
this.scale = scale;
this.isNative = true;
this.index = index;
}
/**
* Constructs a new Scale with given scale, native and index values.
* @param scale as defined in WMTS (scaleDenominator)
* @param isNative is this scale native to the source or not
* @param index zoom index for this scale
*/
public Scale(double scale, boolean isNative, int index) {
this.scale = scale;
this.isNative = isNative;
this.index = index;
}
@Override
public String toString() {
return String.format("%f [%s]", scale, isNative);
}
/**
* Get index of this scale in a {@link ScaleList}
* @return index
*/
public int getIndex() {
return index;
}
public double getScale() {
return scale;
}
}
/**
* List of scales, may include intermediate steps between native resolutions
*/
class ScaleList {
private final List<Scale> scales = new ArrayList<>();
protected ScaleList() {
}
public ScaleList(Collection<Double> scales) {
int i = 0;
for (Double scale: scales) {
this.scales.add(new Scale(scale, i++));
}
}
protected void addScale(Scale scale) {
scales.add(scale);
}
/**
* Returns a ScaleList that has intermediate steps between native scales.
* Native steps are split to equal steps near given ratio.
* @param ratio user defined zoom ratio
* @return a {@link ScaleList} with intermediate steps
*/
public ScaleList withIntermediateSteps(double ratio) {
ScaleList result = new ScaleList();
Scale previous = null;
for (Scale current: this.scales) {
if (previous != null) {
double step = previous.scale / current.scale;
double factor = Math.log(step) / Math.log(ratio);
int steps = (int) Math.round(factor);
if (steps != 0) {
double smallStep = Math.pow(step, 1.0/steps);
for (int j = 1; j < steps; j++) {
double intermediate = previous.scale / Math.pow(smallStep, j);
result.addScale(new Scale(intermediate, false, current.index));
}
}
}
result.addScale(current);
previous = current;
}
return result;
}
/**
* Get a scale from this ScaleList or a new scale if zoomed outside.
* @param scale previous scale
* @param floor use floor instead of round, set true when fitting view to objects
* @return new {@link Scale}
*/
public Scale getSnapScale(double scale, boolean floor) {
return getSnapScale(scale, NavigatableComponent.PROP_ZOOM_RATIO.get(), floor);
}
/**
* Get a scale from this ScaleList or a new scale if zoomed outside.
* @param scale previous scale
* @param ratio zoom ratio from starting from previous scale
* @param floor use floor instead of round, set true when fitting view to objects
* @return new {@link Scale}
*/
public Scale getSnapScale(double scale, double ratio, boolean floor) {
if (scales.isEmpty())
return null;
int size = scales.size();
Scale first = scales.get(0);
Scale last = scales.get(size-1);
if (scale > first.scale) {
double step = scale / first.scale;
double factor = Math.log(step) / Math.log(ratio);
int steps = (int) (floor ? Math.floor(factor) : Math.round(factor));
if (steps == 0) {
return new Scale(first.scale, first.isNative, steps);
} else {
return new Scale(first.scale * Math.pow(ratio, steps), false, steps);
}
} else if (scale < last.scale) {
double step = last.scale / scale;
double factor = Math.log(step) / Math.log(ratio);
int steps = (int) (floor ? Math.floor(factor) : Math.round(factor));
if (steps == 0) {
return new Scale(last.scale, last.isNative, size-1+steps);
} else {
return new Scale(last.scale / Math.pow(ratio, steps), false, size-1+steps);
}
} else {
Scale previous = null;
for (int i = 0; i < size; i++) {
Scale current = this.scales.get(i);
if (previous != null && scale <= previous.scale && scale >= current.scale) {
if (floor || previous.scale / scale < scale / current.scale) {
return new Scale(previous.scale, previous.isNative, i-1);
} else {
return new Scale(current.scale, current.isNative, i);
}
}
previous = current;
}
return null;
}
}
/**
* Get new scale for zoom in/out with a ratio at a number of times.
* Used by mousewheel zoom where wheel can step more than one between events.
* @param scale previois scale
* @param ratio user defined zoom ratio
* @param times number of times to zoom
* @return new {@link Scale} object from {@link ScaleList} or outside
*/
public Scale scaleZoomTimes(double scale, double ratio, int times) {
Scale next = getSnapScale(scale, ratio, false);
int abs = Math.abs(times);
for (int i = 0; i < abs; i++) {
if (times < 0) {
next = getNextIn(next, ratio);
} else {
next = getNextOut(next, ratio);
}
}
return next;
}
/**
* Get new scale for zoom in.
* @param scale previous scale
* @param ratio user defined zoom ratio
* @return next scale in list or a new scale when zoomed outside
*/
public Scale scaleZoomIn(double scale, double ratio) {
Scale snap = getSnapScale(scale, ratio, false);
return getNextIn(snap, ratio);
}
/**
* Get new scale for zoom out.
* @param scale previous scale
* @param ratio user defined zoom ratio
* @return next scale in list or a new scale when zoomed outside
*/
public Scale scaleZoomOut(double scale, double ratio) {
Scale snap = getSnapScale(scale, ratio, false);
return getNextOut(snap, ratio);
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
for (Scale s: this.scales) {
stringBuilder.append(s.toString() + '\n');
}
return stringBuilder.toString();
}
private Scale getNextIn(Scale scale, double ratio) {
if (scale == null)
return null;
int nextIndex = scale.getIndex() + 1;
if (nextIndex <= 0 || nextIndex > this.scales.size()-1) {
return new Scale(scale.scale / ratio, nextIndex == 0, nextIndex);
} else {
Scale nextScale = this.scales.get(nextIndex);
return new Scale(nextScale.scale, nextScale.isNative, nextIndex);
}
}
private Scale getNextOut(Scale scale, double ratio) {
if (scale == null)
return null;
int nextIndex = scale.getIndex() - 1;
if (nextIndex < 0 || nextIndex >= this.scales.size()-1) {
return new Scale(scale.scale * ratio, nextIndex == this.scales.size()-1, nextIndex);
} else {
Scale nextScale = this.scales.get(nextIndex);
return new Scale(nextScale.scale, nextScale.isNative, nextIndex);
}
}
}
}