/*
* Copyright 2014 MovingBlocks
*
* 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 org.terasology.polyworld.elevation;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.terasology.polyworld.graph.Corner;
import org.terasology.polyworld.graph.Graph;
import org.terasology.polyworld.water.WaterModel;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
/**
* TODO Type description
*/
public class DefaultElevationModel extends AbstractElevationModel {
private Graph graph;
private final Map<Corner, Float> elevations = Maps.newHashMap();
private final WaterModel waterModel;
/**
* @param graph the polygon graph
* @param waterModel the water model that defines ocean regions
* @param scale a non-linear scale factor to adjust the height distribution in [0..1]
*/
public DefaultElevationModel(Graph graph, WaterModel waterModel, float scale) {
this.graph = graph;
this.waterModel = waterModel;
List<Corner> landCorners = Lists.newArrayList();
for (Corner c : graph.getCorners()) {
if (!waterModel.isOcean(c) && !waterModel.isCoast(c)) {
landCorners.add(c);
}
}
assignCornerElevations();
redistributeElevationsInverse(landCorners, scale);
for (Corner c : graph.getCorners()) {
if (waterModel.isCoast(c)) {
elevations.put(c, 0.0f);
}
// some of ocean corners that are part of a bay are elevated
// this is more of a workaround rather than a required operation
if (waterModel.isOcean(c)) {
elevations.put(c, -1f);
}
}
}
private void assignCornerElevations() {
Deque<Corner> queue = new LinkedList<>();
for (Corner c : graph.getCorners()) {
if (c.isBorder()) {
elevations.put(c, -1.0f);
queue.add(c);
} else {
elevations.put(c, Float.POSITIVE_INFINITY);
}
}
while (!queue.isEmpty()) {
iterateCorners(queue, queue);
}
}
private void iterateCorners(Deque<Corner> input, Deque<Corner> output) {
while (!input.isEmpty()) {
Corner c = input.pop();
for (Corner a : c.getAdjacent()) {
// adding the extra 0.01f is necessary to make the steepest
// descent towards the ocean. I can't really tell why.
float newElevation = elevations.get(c) + 0.01f;
if (!waterModel.isWater(c) && !waterModel.isWater(a)) {
newElevation += 1;
}
Float prevElevation = elevations.get(a);
if (newElevation < prevElevation) {
elevations.put(a, newElevation);
output.add(a);
}
}
}
}
private void redistributeElevationsLinear(List<Corner> landCorners, float scale) {
if (landCorners.isEmpty()) {
return;
}
// sort land corners by elevation
Corner peak = Collections.max(landCorners, new Comparator<Corner>() {
@Override
public int compare(Corner o1, Corner o2) {
Float e1 = elevations.get(o1);
Float e2 = elevations.get(o2);
return e1.compareTo(e2);
}
});
float maxHeight = elevations.get(peak);
for (int i = 0; i < landCorners.size(); i++) {
Corner corner = landCorners.get(i);
elevations.put(corner, elevations.get(corner) / maxHeight * scale);
}
}
private void redistributeElevationsInverse(List<Corner> landCorners, float scale) {
// sort land corners by elevation
Collections.sort(landCorners, new Comparator<Corner>() {
@Override
public int compare(Corner o1, Corner o2) {
Float e1 = elevations.get(o1);
Float e2 = elevations.get(o2);
return e1.compareTo(e2);
}
});
// reset the elevation x of each to match the inverse of the desired cumulative distribution:
// y(x) = 1 - (1-x)^2
// --> solve for x
// x = 1 - sqrt(1 - y)
int count = landCorners.size();
final float scaleFactor = 1.1f;
for (int i = 0; i < count; i++) {
// y is the relative position in the sorted list
// special case with only one land corner on the island: avoid division by zero explicitly
float y = (count == 1) ? 1 : (float) i / (count - 1);
// x is the desired elevation
float x = scale * (float) (Math.sqrt(scaleFactor) - Math.sqrt(scaleFactor * (1 - y)));
// clamp to max 1
x = Math.min(x, 1);
// this preserves ordering so that elevations always increase from the coast to the mountains.
elevations.put(landCorners.get(i), x);
}
}
@Override
public float getElevation(Corner corner) {
return elevations.get(corner);
}
}