I'm new to geotools and somewhat new to Java. I have created a program that can read a geotiff, crop it, and, using JavaFX, render the cropped image into an ImageView. Now, I'd like to add geographical points as layers on the rendered image. I've accomplished creating a MapContent with a title. Where I am having issues, is rendering a JMapFrame to test the data is being passed. I am trying to create and add a GridCoverageLayer of the cropped image. I cannot get the JMapFrame to render the image, it appears to be stuck in a loop. I am suspecting the issue is setting the Style of the Layer to NULL. If this is the issue, how do I create a raster based Style? I've tried reading the Geotools API and tutorials, and I just can't make heads or tails half the time....
My ultimate goal is render the map with symbols with JavaFX instead of AWT.
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.processing.CoverageProcessor;
import org.geotools.gce.geotiff.GeoTiffReader;
import org.geotools.geometry.GeneralDirectPosition;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.GridCoverageLayer;
import org.geotools.map.Layer;
import org.geotools.map.MapContent;
import org.geotools.referencing.GeodeticCalculator;
import org.geotools.swing.JMapFrame;
import org.geotools.util.factory.Hints;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import java.io.File;
import java.io.IOException;
public class Processor {
private static void getImage(File file, double NE_lon, double NE_lat, double SW_lon, double SW_lat) throws IOException, TransformException{
//Create the coverage processor and create the crop operation
final CoverageProcessor processor = new CoverageProcessor();
final ParameterValueGroup param = processor.getOperation("CoverageCrop").getParameters();
//Read the TIFF, create the coverage/grid, get the CRS, and get the image envelope
GeoTiffReader reader = new GeoTiffReader(file, new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER,Boolean.TRUE));
GridCoverage2D coverage = reader.read(null);
CoordinateReferenceSystem inCRS = coverage.getCoordinateReferenceSystem();
GeneralEnvelope inEnvelope = (GeneralEnvelope) coverage.getEnvelope();
//Get the image envelope min/max coordinates
GeneralDirectPosition inMaxDP = (GeneralDirectPosition) inEnvelope.getUpperCorner();
GeneralDirectPosition inMinDP = (GeneralDirectPosition) inEnvelope.getLowerCorner();
//Calculate the crop cartesian min/max coordinates
GeodeticCalculator calc = new GeodeticCalculator(inCRS);
calc.setStartingGeographicPoint(NE_lon,NE_lat);
GeneralDirectPosition cropMaxDP = (GeneralDirectPosition) calc.getStartingPosition();
calc.setStartingGeographicPoint(SW_lon,SW_lat);
GeneralDirectPosition cropMinDP = (GeneralDirectPosition) calc.getStartingPosition();
//Output to console the original and cropped cartesian min/max coordinates
System.out.println("Coordinate system: ");
System.out.println("NE (max) corner (meters from meridian (x), origin (y): "+inMaxDP);
System.out.println("SW (min) corner (meters from meridian (x), origin (y): "+inMinDP);
System.out.println();
System.out.println("NE (max) trim corner (lon,lat): "+NE_lon+","+NE_lat);
System.out.println("SW (min) trim corner (lon,lat): "+SW_lon+","+SW_lat);
System.out.println("NE (max) trim corner (meters from meridian (x), origin (y): "+cropMaxDP);
System.out.println("SW (min) trim corner (meters from meridian (x), origin (y): "+cropMinDP);
System.out.println();
//Create the crop envelope size and crop the image envelope
final ReferencedEnvelope crop = new ReferencedEnvelope(
cropMinDP.getOrdinate(0),
cropMaxDP.getOrdinate(0),
cropMinDP.getOrdinate(1),
cropMaxDP.getOrdinate(1),
inCRS);
//Set the Processor to look at the Coverage2D image and crop to the ReferenceEnvelope set
param.parameter("Source").setValue( coverage );
param.parameter("Envelope").setValue( crop );
GridCoverage2D cropCoverage = (GridCoverage2D) processor.doOperation(param);
//Create a Map with layers
MapContent map = new MapContent();
map.setTitle("Detroit");
Layer coverageLayer = new GridCoverageLayer(cropCoverage,null,"Background");
map.addLayer(coverageLayer);
JMapFrame.showMap(map);
//Generate a BufferedImage of the GridCoverage2D
// PlanarImage croppedRenderedImageImage = (PlanarImage) cropCoverage.getRenderedImage();
// BufferedImage image = croppedRenderedImageImage.getAsBufferedImage();
// System.out.println("Image type: "+image.getType());
// System.out.println("Image height: "+image.getHeight());
// System.out.println("Image width: "+image.getWidth());
//Write crop to file system
/*File outFile = new File("/home/greg/Software_Projects/JavaProjects/charts/Detroit_98/Detroit_SEC_98.tif");
GeoTiffWriter writer = new GeoTiffWriter(outFile,new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER,Boolean.TRUE));
writer.write(cropped,null);*/
}
public static void main(String[] args) throws IOException, TransformException {
File inFile = new File("/home/greg/Software_Projects/JavaProjects/charts/Detroit_98/Detroit SEC 98.tif");
getImage(inFile,-81,42,-82.5,41);
}
}
EDIT
GdalInfo for image
Image Structure Metadata: INTERLEAVE=BAND
Corner Coordinates: Upper Left ( -84165.569, -73866.808) ( 82d 0'29.23"W, 41d29'48.29"N)
Lower Left ( -84165.569, -129071.257) ( 82d 0' 0.36"W, 40d59'59.09"N)
Upper Right ( -41702.491, -73866.808) ( 81d29'58.28"W, 41d30' 0.88"N)
Lower Right ( -41702.491, -129071.257) ( 81d29'43.98"W, 41d 0'11.57"N)
Center ( -62934.030, -101469.032) ( 81d45' 2.94"W, 41d15' 0.94"N)
Band 1 Block=1003x1 Type=Byte, ColorInterp=Palette NoData Value=0 Color Table (RGB with 256 entries)
Update
Your tiff is not a simple raster, it contains a paletted image (ColorInterp=Palette) so each pixel contains a single byte between 0-255 which maps to a colour. So you aim to symbolize the image will not work as there is not a linear relationship between pixel values and colours. To display this image in GeoTools you need an empty RasterSymbolizer which is what the createGreyscaleStyle() method does. I've tested it with a paletted image and it works fine for me (note bands count from 1 and you only have one band).
private Style createGreyscaleStyle(int band) {
ContrastEnhancement ce = new ContrastEnhancementImpl();
SelectedChannelType sct = sf.createSelectedChannelType(String.valueOf(band), ce);
RasterSymbolizer sym = sf.getDefaultRasterSymbolizer();
ChannelSelection sel = sf.channelSelection(sct);
sym.setChannelSelection(sel);
return SLD.wrapSymbolizers(sym);
}
Section 4 of the Image Tutorial shows how to create a colour raster SLD - you can't just use a NULL style as GeoTools will now have no idea of how to convert the bands to an image. There is a fuller description of possible RasterSymbolizer options in the SLD reference in the GeoServer manual. Alternatively, you can import an SLD file containing the style.
/**
* This method examines the names of the sample dimensions in the provided coverage looking for
* "red...", "green..." and "blue..." (case insensitive match). If these names are not found it
* uses bands 1, 2, and 3 for the red, green and blue channels. It then sets up a raster
* symbolizer and returns this wrapped in a Style.
*
* #return a new Style object containing a raster symbolizer set up for RGB image
*/
private Style createRGBStyle() {
GridCoverage2D cov = null;
try {
cov = reader.read(null);
} catch (IOException giveUp) {
throw new RuntimeException(giveUp);
}
// We need at least three bands to create an RGB style
int numBands = cov.getNumSampleDimensions();
if (numBands < 3) {
return null;
}
// Get the names of the bands
String[] sampleDimensionNames = new String[numBands];
for (int i = 0; i < numBands; i++) {
GridSampleDimension dim = cov.getSampleDimension(i);
sampleDimensionNames[i] = dim.getDescription().toString();
}
final int RED = 0, GREEN = 1, BLUE = 2;
int[] channelNum = {-1, -1, -1};
// We examine the band names looking for "red...", "green...", "blue...".
// Note that the channel numbers we record are indexed from 1, not 0.
for (int i = 0; i < numBands; i++) {
String name = sampleDimensionNames[i].toLowerCase();
if (name != null) {
if (name.matches("red.*")) {
channelNum[RED] = i + 1;
} else if (name.matches("green.*")) {
channelNum[GREEN] = i + 1;
} else if (name.matches("blue.*")) {
channelNum[BLUE] = i + 1;
}
}
}
// If we didn't find named bands "red...", "green...", "blue..."
// we fall back to using the first three bands in order
if (channelNum[RED] < 0 || channelNum[GREEN] < 0 || channelNum[BLUE] < 0) {
channelNum[RED] = 1;
channelNum[GREEN] = 2;
channelNum[BLUE] = 3;
}
// Now we create a RasterSymbolizer using the selected channels
SelectedChannelType[] sct = new SelectedChannelType[cov.getNumSampleDimensions()];
ContrastEnhancement ce = sf.contrastEnhancement(ff.literal(1.0), ContrastMethod.NORMALIZE);
for (int i = 0; i < 3; i++) {
sct[i] = sf.createSelectedChannelType(String.valueOf(channelNum[i]), ce);
}
RasterSymbolizer sym = sf.getDefaultRasterSymbolizer();
ChannelSelection sel = sf.channelSelection(sct[RED], sct[GREEN], sct[BLUE]);
sym.setChannelSelection(sel);
return SLD.wrapSymbolizers(sym);
}
}
Related
I'm currently working with a folder of JSON files which are collected through a tracking experiment with a drone. The data contains position, rotation and timestamp of the drone while it's moving and levitating inside the tracking system.
What I'm currently doing is trying to simulate the movement of the drone inside Unity using those data. So far, I've managed to parse the position and rotation from the data to an object inside Unity and extract the timestamp to System.DateTime in Unity.
However, I don't how to work with the timestamp. I want to use the timestamp to match the position and rotation of the object (i.e: at this timestamp, the drone should be at this position(x,y,z) and has the rotation(x,y,z,w)). Can someone help me with this problem, really appreciate your help :D Here is my current code:
void Update()
{
if (loaded)
{
for(int i = 0; i <= pos_data.Count; i+= 10)
{
Cube.transform.position = pos_data[i];
Cube.transform.rotation = rot_data[i];
}
}
else
{
LoadJson();
//startTime = datetime[0];
loaded = true;
}
}
public void LoadJson()
{
string HeadPath = #Application.dataPath + "/Data/" + "drone_data_1.json";
string HeadJsonhold = File.ReadAllText(HeadPath);
var data_ = JSON.Parse(HeadJsonhold);
for (int rows = 0; rows <= data_.Count; rows += 10)
{
pos_data.Add(new Vector3(data_[rows]["location"]["x"].AsFloat, data_[rows]["location"]["y"].AsFloat, data_[rows]["location"]["z"].AsFloat));
rot_data.Add(new Quaternion(data_[rows]["rotation"]["x"].AsFloat, data_[rows]["rotation"]["y"].AsFloat, data_[rows]["rotation"]["z"].AsFloat, data_[rows]["rotation"]["w"].AsFloat));
Time = System.DateTime.ParseExact(data_[rows]["Timestamp"], "yyyyMMddHHmmss",null);
//Debug.Log(Time);
}
}
If I understand you correctly what you are getting are samples of a real-world drone that at some rate stores keyframes of its movement.
Now you already successfully load that json data but wonder how to animate the Unity object accordingly.
The timestamp itself you can't use at all! ^^
It most probably lies somewhere in the past ;) And you can't just assign something to Time.
What you can do, however, is take the timestamp of the first sample (I will just assume that your samples are all already ordered by the time) and calculate the difference to the next sample and so on.
Then you can use that difference in order to always interpolate between the current and next sample transforms using the given time delta.
Currently you are just doing all samples in one single frame so there won't be any animation at all.
Also just as sidenote:
for(int i = 0; i <= pos_data.Count; i+= 10)
is wrong twice:
a) you already skipped 10 samples when loading the data -> are you sure you now want to again skip 10 of these => In total every time skipping 100 samples?
b) since indices are 0 based the last accessible index would be pos_data.Count - 1 so in general when iterating Lists/arrays it should be i < pos_data.Count ;)
First of all I would suggest you use a better data structure and use one single list holding the information that belongs together instead of multiple parallel lists and rather load your json like e.g.
[Serializable]
public class Sample
{
public readonly Vector3 Position;
public readonly Quaternion Rotation;
public readonly float TimeDelta;
public Sample(Vector3 position, Quaternion rotation, float timeDelta)
{
Position = position;
Rotation = rotation;
TimeDelta = timeDelta;
}
}
And then
// Just making this serialized so you can immediately see in the Inspector
// if your data loaded correctly
[SerializeField] private readonly List<Sample> _samples = new List<Sample>();
public void LoadJson()
{
// start fresh
_samples.Clear();
// See https://learn.microsoft.com/dotnet/api/system.io.path.combine
var path = Path.Combine(Application.dataPath, "Data", "drone_data_1.json");
var json = File.ReadAllText(path);
var data = JSON.Parse(json);
DateTime lastTime = default;
for (var i = 0; i <= data.Count; i += 10)
{
// First I would pre-cache these values
var sample = data[i];
var sampleLocation = sample["location"];
var sampleRotation = sample["rotation"];
var sampleTime = sample["Timestamp"];
// Get your values as you did already
var position = new Vector3(sampleLocation["x"].AsFloat, sampleLocation["y"].AsFloat, sampleLocation["z"].AsFloat));
var rotation = new Quaternion(sampleRotation["x"].AsFloat, sampleRotation["y"].AsFloat, sampleRotation["z"].AsFloat, sampleRotation["w"].AsFloat));
var time = System.DateTime.ParseExact(sampleTime, "yyyyMMddHHmmss", null);
// Now for the first sample there is no deltaTime
// for all others calculate the difference in seconds between the
// last and current sample
// See https://learn.microsoft.com/dotnet/csharp/language-reference/operators/conditional-operator
var deltaTime = i == 0 ? 0f : GetDeltaSeconds(lastTime, time);
// and of course store it for the next iteration
lastTime = time;
// Now you can finally add the sample to the list of samples
// instead of having multiple parallel lists
_samples.Add(new Sample(position, rotation, deltaTime));
}
}
private float GetDeltaSeconds(DateTime first, DateTime second)
{
// See https://learn.microsoft.com/dotnet/api/system.datetime.op_subtraction#System_DateTime_op_Subtraction_System_DateTime_System_DateTime_
var deltaSpan = second - first;
// See https://learn.microsoft.com/dotnet/api/system.timespan.totalseconds#System_TimeSpan_TotalSeconds
return (float)deltaSpan.TotalSeconds;
}
So now what to do with this information?
You now have samples (still assuming ordered by time) holding all required information to be able to interpolate between them.
I would use Coroutines instead of Update, in my eyes they are easier to understand and maintain
// Do your loading **once** in Start
private void Start()
{
LoadJson();
// Then start the animation routine
// I just make it a method so you could also start it later e.g. via button etc
StartAnimation();
}
// A flag just in case to avoid concurrent animations
private bool alreadyAnimating;
// As said just making this a method so you could also remove it from Start
// and call it in any other moment you like
public void StartAnimation()
{
// Only start an animation if there isn't already one running
// See https://docs.unity3d.com/ScriptReference/MonoBehaviour.StartCoroutine.html
if(!alreadyAnimating) StartCoroutine(AnimationRoutine());
}
private IEnumerator AnimationRoutine()
{
// Just in case abort if there is already another animation running
if(alreadyAnimating) yield break;
// Block concurrent routine
alreadyAnimating = true;
// Initially set your object to the first sample
var lastSample = _samples[0];
Cube.transform.position = lastSample.Position;
Cube.transform.rotation = lastSample.Rotation;
// This tells Unity to "pause" the routine here, render this frame
// and continue from here in the next frame
yield return null;
// then iterate through the rest of samples
for(var i = 1; i < _samples.Count; i++)
{
var lastPosition = lastSample.Position;
var lastRottaion = lastSample.Rottaion;
var currentSample = _samples[i];
var targetPosition = sample.Position;
var targetRotation = sample.Rotation;
// How long this interpolation/animation will take
var duration = currentSample.TimeDelta;
// You never know ;)
// See https://docs.unity3d.com/ScriptReference/Mathf.Approximately.html
if(Mathf.Approximately(duration, 0f))
{
Cube.transform.position = targetPosition;
Cube.transform.rotation = targetRotation;
lastSample = currentSample;
continue;
}
// And this is where the animation magic happens
var timePassed = 0f;
while(timePassed < duration)
{
// this factor will be growing linear between 0 and 1
var factor = timePassed / duration;
// Interpolate between the "current" transforms (the ones it had at beginning of this iteration)
// towards the next sample target transforms using the factor between 0 and 1
// See https://docs.unity3d.com/ScriptReference/Vector3.Lerp.html
Cube.transform.position = Vector3.Lerp(lastPosition, targetPosition, factor);
// See https://docs.unity3d.com/ScriptReference/Quaternion.Slerp.html
Cube.transform.rotation = Quaternion.Slerp(lastRotation, targetRotation, factor);
// This tells Unity to "pause" the routine here, render this frame
// and continue from here in the next frame
yield return null;
// increase by the time passed since the last frame was rendered
timePassed += Time.deltaTime;
}
// just to be sure to end with clean values
Cube.transform.position = targetPosition;
Cube.transform.rotation = targetRotation;
lastSample = currentSample;
}
// Allow the next animation to start (or restart this one)
alreadyAnimating = false;
// Additional stuff to do once the animation is done
}
Helly everyone! I'm trying to dynamically add (and later remove) some movieclips inside of a triangle. Simple movieclip inside of a movieclip ain't working (it's a square in the end). Drawing a triangle is simple, addChild method is crystal clear too. The tough part comes after. Here's the code I'm trying to develop:
btn_toys_2.confirm.addEventListener(MouseEvent.MOUSE_UP, confirmToys);
import flash.display.Graphics;
var point1:Point = new Point(466, 65);
var point2:Point = new Point(370, 540);
var point3:Point = new Point(570, 540);
var vertices:Vector.<Number> = Vector.<Number>([point1.x, point1.y, point2.x, point2.y, point3.x, point3.y]);
var triangle:Sprite = new Sprite();
triangle.graphics.beginFill(0x00ff00, 1);
triangle.graphics.drawTriangles(vertices);
triangle.graphics.endFill();
addChild(triangle);
function confirmToys(e:MouseEvent){
var toy:MovieClip = new shar_001;
triangle.addChild(toy);
toy.x = Math.random()*30;
toy.y = Math.random()*30;
}
The "toy" movieclip is for some reason placed outside the triangle (0-30 x axis and 0-30 y axis).
The important part is to make "toys" appear within a triangle, doesnt have to be a movieclip. A way around this would be great too!
Thanks in advance!
It is happening this way because you have made your triangles anchor point at zero. You did this when you did
addChild(triangle);
That will always put the added child at (0,0). The only reason you triangle doesn't appear there is because you have added a cushion of empty pixels by making your points be greater than zero. Instead, you will use
addChild(triangle);
triangle.x = 370;
triangle.y = 65;
The point you want the top left corner of your triangle to be at is (370, 65). You should make your triangle points be (96, 0), (0, 475), (200, 475). Now the top left corner of the triangle is at (0,0) on the stage. Now set the triangle to (370, 65) after adding the triangle to the stage. Now the triangles anchor point is still the top left corner of the triangle, not the stage, so when you add the toy, it will be in reference to the point you expect.
// let the minimum x and y be zero, and adjust the others relative to that.
var point1:Point = new Point(96, 0);
var point2:Point = new Point(0, 475);
var point3:Point = new Point(200, 475);
var toyArray:Array = new Array();
var vertices:Vector.<Number> = Vector.<Number>([point1.x, point1.y, point2.x, point2.y, point3.x, point3.y]);
var triangle:Sprite = new Sprite();
triangle.graphics.beginFill(0x00ff00, 1);
triangle.graphics.drawTriangles(vertices);
triangle.graphics.endFill();
addChild(triangle);
// position anchor point on stage
triangle.x = 370;
triangle.y = 65;
function confirmToys(e:MouseEvent){
var p:Point = new Point(Math.random()*triangle.width,Math.random()*triangle.height);
if (isInsideTriangle(Point1,Point2,Point3,p))
{
var toy:MovieClip = new shar_001;
triangle.addChild(toy);
toyArray.push(toy);
toy.x = p.x;
toy.y = p.y;
}
}
private function isInsideTriangle(A:Point,B:Point,C:Point,P:Point):Boolean {
var planeAB:Number = (A.x-P.x)*(B.y-P.y)-(B.x-P.x)*(A.y-P.y);
var planeBC:Number = (B.x-P.x)*(C.y-P.y)-(C.x - P.x)*(B.y-P.y);
var planeCA:Number = (C.x-P.x)*(A.y-P.y)-(A.x - P.x)*(C.y-P.y);
return sign(planeAB)==sign(planeBC) && sign(planeBC)==sign(planeCA);
}
private function sign(n:Number):int {
return Math.abs(n)/n;
}
Removing the toys from the triangle should be pretty straight forward depending on the method you want to use. I added a toyArray that you can iterate through to remove them.
Checking if a position is within the desired boundaries and rejecting it if it's not is certainly a solution. However, this stops the program from being deterministic, because you never know how many tries it takes before a position within the boundaries is found.
Does that mean the program could run forever? Possibly yes, but this is so unlikely that it's not going to happen. Depending on how much of its bounding box a triangle fills, it will still produce quite a few misses though. Misses that have to be checked, rejected and tried again.
I'm not advising against this strategy because it might be a performance problem (and it might actually be one), but rather because it seems to miss the point: if positions in a triangle should be found, let's just do exactly that. All this trial and error and testing and rejecting is counterintuitive.
You only have one pseudo random number generator built in: Math.random().
That produces an evenly distributed random number between 0 and 1. (let's ignore whether the boundaries are possible values or not)
To create a 2D distribution, it's very easy to simply use two of those.
Now the problem with the even distribution is that it's even. To form a non-rectangular shape, transformations have to be applied.
Consider two edges of the triangle to be two vectors. A random point in the triangle is found by combining those two vectors linearly in a random way.
Obviously, with the untransformed random numbers, that would yield a diamond shaped boundary for the random points. To compensate for the fact that the vectors meet at one point and diverge in the other direction the square root is applied to one random number. The math behind that is not too complicated but ain't trivial either. I choose to omit it here. For more information ask a new question, math.se is probably a good place to do this.
Here's a full fledged example code to be used as a document class, which puts 1000 circles into a triangle boundary:
package
{
import flash.display.Shape;
import flash.display.Sprite;
import flash.geom.Point;
public class Main extends Sprite
{
public function Main()
{
var distribution:EvenDistribution2DTriangleBoundary = new EvenDistribution2DTriangleBoundary(new Point(100, 100), new Point(400, 50), new Point(250, 350));
for (var i:int = 0; i < 1000; ++i)
{
// create an object in each iteration of the loop
var circle:Shape = new Shape();
//add some graphics (this is unnecessary if you use a library symbol)
circle.graphics.beginFill(0xff0000, .6);
circle.graphics.drawCircle(0, 0, 3);
circle.graphics.endFill();
// add it to the display list
addChild(circle);
// reposition it with the help of the distribution object
distribution.positionDisplayObject(circle);
}
}
}
}
import flash.display.DisplayObject;
import flash.geom.Point;
internal class EvenDistribution2DTriangleBoundary
{
private var u:Point;
private var v:Point;
private var position:Point;
public function EvenDistribution2DTriangleBoundary(a:Point, b:Point, c:Point)
{
// consider corner "a" as the position of the triangle, this is arbitrary decision, but has to be consistent with the rest of this constructor
position = a;
// create two vectors from the corner that is the position to the other two corners respectively
u = b.subtract(a);
v = c.subtract(a);
}
public function getRandomPosition():Point
{
// random position formula with two random variables: position + (u + (v-u) * random1) * sqrt(random2)
var r1:Number = Math.random();
// the sqrt transforms the probability density function of the even distribution f(x) = 1 into a triangle g(y) = 2y
var r2:Number = Math.sqrt(Math.random());
// applying the above formula to create an evenly distributed random position within the triangle
return position.add(new Point((u.x + (v.x - u.x) * r1) * r2, (u.y + (v.y - u.y) * r1) * r2));
}
// convenience function to position a display object at a random position in the triangle
public function positionDisplayObject(object:DisplayObject):void
{
var position:Point = getRandomPosition();
object.x = position.x;
object.y = position.y;
}
}
Creating the random distribution is a class of its own. For the sake of simple testing, it's an internal class, thus the entire example is a single class that goes into a single file. Of course, in production code, this should be better organised.
Here are 4 results that I got:
it seems I need to transfer all of my frames/timeline code (and there's a lot!) into the external class
That isn't necessary although it is recommended. You should eventually only use class based code, but of course making that transition within a project isn't very practical.
In my example above, there are two classes: Main and EvenDistribution2DTriangleBoundary. You are only interested in the latter one. Main is just there to use the other class, create and display the circles, etc: it's a demo.
To use EvenDistribution2DTriangleBoundary in your project, create a new text file in the same directory as your .fla file named EvenDistribution2DTriangleBoundary.as with the following content:
package
{
import flash.display.DisplayObject;
import flash.geom.Point;
public class EvenDistribution2DTriangleBoundary
{
private var u:Point;
private var v:Point;
private var position:Point;
public function EvenDistribution2DTriangleBoundary(a:Point, b:Point, c:Point)
{
position = a;
u = b.subtract(a);
v = c.subtract(a);
}
public function getRandomPosition():Point
{
var r1:Number = Math.random();
var r2:Number = Math.sqrt(Math.random());
return position.add(new Point((u.x + (v.x - u.x) * r1) * r2, (u.y + (v.y - u.y) * r1) * r2));
}
public function positionDisplayObject(object:DisplayObject):void
{
var position:Point = getRandomPosition();
object.x = position.x;
object.y = position.y;
}
}
}
Now you can use that class like any other class in your project. For example, you can add the code from Main's constructor to your timeline and it should work:
var distribution:EvenDistribution2DTriangleBoundary = new EvenDistribution2DTriangleBoundary(new Point(100, 100), new Point(400, 50), new Point(250, 350));
for (var i:int = 0; i < 1000; ++i)
{
// create an object in each iteration of the loop
var circle:Shape = new Shape();
//add some graphics (this is unnecessary if you use a library symbol)
circle.graphics.beginFill(0xff0000, .6);
circle.graphics.drawCircle(0, 0, 3);
circle.graphics.endFill();
// add it to the display list
addChild(circle);
// reposition it with the help of the distribution object
distribution.positionDisplayObject(circle);
}
You should see a triangle of red circles similar to those I posted in the image of results above. Does that work?
I am working on a Libgdx game which loads Tiled maps. The current map I am working on makes use of 2 tilesets, one for shadow/light and another for terrain and buildings. The general process I do, that has been working fine, is that I receive the sprite sheet from the artist, design the maps, then take the spritesheet file and split it using ImageMagick. From there I take the split images and create an optimized png and atlas file with TexturePacker.
However, this is the first map I have made that makes use of multiple tilesets. The issue I am having is when loading the map with AtlasTmxMapLoader it relies on a single atlas file property in the map. My shadows and lighting are split into a separate image and atlas and Id rather not merge them all into one in Tiled (and have to re-do a portion of the map).
Perhaps I am missing something simple. How can I handle multiple tilesets?
So after reading more into how .tmx files are read I was able to fix my problem.
Here is how to properly do it when working with multiple tilesets and re-packing your spritesheets in TexturePacker. First, cut up the tileset images using a utility like ImageMagick and make sure they are indexed (specified by an underscore and number in the filename). You can do this with the crop command in ImageMagick like so:
convert.exe "shrine_tileset.png" -crop 16x16 "shrine_tileset_%02d.png"
Second, re-pack all tiles from all tilesets into a single atlas in TexturePacker. If it works correctly you will see the name of each tileset in the atlas file with an associated index based on the tile id. For example:
shrine_tileset
rotate: false
xy: 382, 122
size: 16, 16
orig: 16, 16
offset: 0, 0
index: 703
Finally (and this is the part I could not figure out), make sure each tileset's tile indexes start from the "firstgid" value in the .tmx file. For example, my second tilesheet starts from 2049, as their are 2048 tiles in the first sheet. This should be denoted at the top of the .tmx file for each tileset.
<tileset firstgid="2049" source="shadow_light.tsx"/>
So when cutting up the tiles for my tileset "shadow_light", I would start them from index 2048, one less than the gid, EX: "shadow_light_2048.png".
Hopefully this helps someone!
I am no LibGDX expert but almost all tilemap renderers I've seen rely on working with 1 tileset. The reason is that they are rendered using OpenGL. The renderer sets the texture and draws all tiles with 1 draw call. You can't switch textures in between.
The best way would be to create 2 (or more) separate layers. Each layer uses 1 tileset. E.g. 1 for the background, 1 for the shadows, 1 for the foreground (e.g. walls).
This issue is fixed in 1.9.11. If you are using an earlier version you can override AtlasTmxMapLoader with a fix.
MyAtlasTmxMapLoader.Java
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.maps.ImageResolver;
import com.badlogic.gdx.maps.MapProperties;
import com.badlogic.gdx.maps.tiled.AtlasTmxMapLoader;
import com.badlogic.gdx.maps.tiled.TiledMapTile;
import com.badlogic.gdx.maps.tiled.TiledMapTileSet;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.SerializationException;
import com.badlogic.gdx.utils.XmlReader.Element;
public class MyAtlasTmxMapLoader extends AtlasTmxMapLoader {
/**
* Same as AtlasTmxMapLoader, but fixed to get the firstid attribute from the tileset element in the TMX file, not tsx file.
*/
#Override
protected void loadTileSet(Element mapElement, FileHandle tmxFile, ImageResolver imageResolver) {
if (mapElement.getName().equals("tileset")) {
String imageSource = "";
int imageWidth = 0;
int imageHeight = 0;
FileHandle image = null;
Element element = null;
String source = mapElement.getAttribute("source", null);
if (source != null) {
FileHandle tsx = getRelativeFileHandle(tmxFile, source);
try {
element = xml.parse(tsx);
Element imageElement = element.getChildByName("image");
if (imageElement != null) {
imageSource = imageElement.getAttribute("source");
imageWidth = imageElement.getIntAttribute("width", 0);
imageHeight = imageElement.getIntAttribute("height", 0);
image = getRelativeFileHandle(tsx, imageSource);
}
} catch (SerializationException e) {
throw new GdxRuntimeException("Error parsing external tileset.");
}
} else {
Element imageElement = mapElement.getChildByName("image");
if (imageElement != null) {
imageSource = imageElement.getAttribute("source");
imageWidth = imageElement.getIntAttribute("width", 0);
imageHeight = imageElement.getIntAttribute("height", 0);
image = getRelativeFileHandle(tmxFile, imageSource);
}
}
String name = element.get("name", null);
// Get the firstid attribute from the tileset element in the TMX file, not tsx file.
int firstgid = mapElement.getIntAttribute("firstgid", 1);
int tilewidth = element.getIntAttribute("tilewidth", 0);
int tileheight = element.getIntAttribute("tileheight", 0);
int spacing = element.getIntAttribute("spacing", 0);
int margin = element.getIntAttribute("margin", 0);
Element offset = element.getChildByName("tileoffset");
int offsetX = 0;
int offsetY = 0;
if (offset != null) {
offsetX = offset.getIntAttribute("x", 0);
offsetY = offset.getIntAttribute("y", 0);
}
TiledMapTileSet tileSet = new TiledMapTileSet();
// TileSet
tileSet.setName(name);
final MapProperties tileSetProperties = tileSet.getProperties();
Element properties = element.getChildByName("properties");
if (properties != null) {
loadProperties(tileSetProperties, properties);
}
tileSetProperties.put("firstgid", firstgid);
// Tiles
Array<Element> tileElements = element.getChildrenByName("tile");
addStaticTiles(tmxFile, imageResolver, tileSet, element, tileElements, name, firstgid, tilewidth,
tileheight, spacing, margin, source, offsetX, offsetY, imageSource, imageWidth, imageHeight, image);
for (Element tileElement : tileElements) {
int localtid = tileElement.getIntAttribute("id", 0);
TiledMapTile tile = tileSet.getTile(firstgid + localtid);
if (tile != null) {
addTileProperties(tile, tileElement);
addTileObjectGroup(tile, tileElement);
addAnimatedTile(tileSet, tile, tileElement, firstgid);
}
}
map.getTileSets().addTileSet(tileSet);
}
}
}
And then call:
new MyAtlasTmxMapLoader().load(pathname)
Source: [Tutorial] Using multiple Tilesets with Libgdx and Tiled
I'm trying to create a loop of cubes and apply a ColorMaterial to the surface. The main actionscript class has the following method for creating the loop. There is an external class file called Building which is responsible for the shape and ColorMaterial. Unfortunately, I seem to be doing something wrong. Flash Builder is not showing any warnings or errors. But when I run a test, all I get is black screen.
private function buildCity():void
{
var citySize:int = 1800;
var buildingSize:int = 100;
var roadSize:int = 50;
// Loop across in the x direction and again in the z direction
for(var cityX:int=citySize/2;cityX<citySize/2;cityX+=buildingSize+roadSize)
{
for(var cityZ:int=citySize/2;cityZ<citySize/2;cityZ+=buildingSize+roadSize)
{
// Create buildings
var building:Building = new Building();
// Position it
building.x = cityX;
building.z = cityZ;
// Add to the scene
view.scene.addChild(building);
}
}
}
The Building class responsible for the shape and material
package
{
import away3d.entities.Mesh;
import away3d.materials.ColorMaterial;
import away3d.primitives.CubeGeometry;
public class Building extends Mesh
{
public function Building()
{
super(new CubeGeometry(50,100,50));
// Make a material
material = new ColorMaterial(Math.random()*0xFFFFFF);
// Offset the y position based on height
y = 50;
}
}
}
Check your loop parameters. For example, in your code above, you set cityX to citySize / 2, then let it iterate while cityX < citySize / 2.
Since cityX >= citySize / 2 from the start, the loop is never executed.
The same goes for the inner loop.
I am making a Sim City like game. There are lots of tiles. When I first started. I was just using a tilesheet. I was copying the necessary pieaces from the tilesheet. on to a blank bitMapData. I then took the bitMapData and put it into a bitMap which I then put into a DisplayObject. It worked great!
tileSheet:BitMapData <----- data is already in
loop { loop through and tiled
bg:bitMapData= new bitMapData();
bg.copyPixel(tileSheet,rect,point);
}
canvas.BitMap(bg);
addChild(canvas);
Only problem was I needed to make my tiles interactive. I needed to highlight them and change colors and stuff. So I used the Sprite object. It works great but I can only have so many on the stage at once. or else it moves slow when I scroll. I need something Lighter then a sprite, but yet I can still turn into a object to make interactive. Anyone have any ideas ???
If you have a lot of tiles, that will impact performance because Flash needs to update the transformations of a lot of display objects (which internally means a lot of matrix calculations, and subsequent redraws of big areas of the screen.)
There is another way to achieve interactivity, if you find that you must use a single bitmap data for performance. Keep an "abstract" (i.e. not graphical) data model in memory, that stores your game state. Make sure that you are able to read from your store where a certain element is positioned in the game world. Then you can use a flat bitmap data to render the game world, because the individual positions are stored elsewhere.
When the user clicks the DisplayObject containing the bitmap data (a Sprite in which the bitmap is drawn using a bitmap fill, or that wraps a Bitmap), look in your model which of your game elements was hit by that click.
// myTileSprite is a Sprite with a bitmap fill
myTileSprite.addEventListener(MouseEvent.CLICK, handleWorldClick);
function handleWorldClick(ev : MouseEvent) : void
{
var i : int;
// Loop through all game element data models
for (i=0; i<myGameElements.length; i++) {
// Test the mouse position against the element model
if (myGameElements[i].hitTest(myTileSprite.mouseX, myTileSprite.mouseY)) {
trace('this was the element that was clicked: '+myGameElements[i].toString());
}
}
}
Here, whenever the player clicks the world graphics, the loop tries to find that element which was directly under the mouse position. You will need to implement a hitTest() method on all your game element data models, of course. Such a method simply checks the supplied world space position against the tile's area:
// GameElement.hitTest():
/**
* Tests a world position against the position and area of this game
* element tile. Returns a boolean indicating whether this tile was hit.
*/
public function hitTest(mouseX : Number, mouseY : Number) : void
{
var rect : Rectangle = new Rectangle(this.worldX, this.worldY, this.width, this.height);
if (mouseX > rect.left && mouseX < rect.right
&& mouseY > rect.top && mouseY < rect.top) {
return true;
}
else return false;
}
The GameElement class is not an display object, but has worldX and worldY properties indicating where it is located in the world. It's width and height properties define it's dimensions.
The trick from hereon is to make sure that the rendered bitmap and your model storage is synchronized, so that a tile's position on the bitmap really corresponds to it's worldX/worldY properties in the data model.
I am one step ahead of you. And that is a great idea. Its alot easier to keep a data representation of the world when the tiles are squared. I therefore can take my mouseX/tileWidth, and thats hw many columns I moved from left to right. same with the Y axis.
Not only that but coordinates start at top left corner.
But issue I have is that my tiles are Isometric. So instead of the X axis start off like...
012345678
0
1
2
3
4
5
6
7
8
My tiles are aligned like...
00
1 1
2 2
3 3
4 4
5 6
its a little sloppy. but the right side represents the y axis and the left represents the x axis. and the center origin is in the center of the screen. not on the top left. I am trying to figure out how to measure where my mouse is from the center and out on both sides. This sounds extremely difficult. I am not sure if its possible. The game is suppose to be like a sim city like game. The first sim city was squares not isometric. I dont think they went isometric until they started using 3d. I wonder if its possible to create a illusion of isometric on a square tile.
Ive been reading this great book on isometrics. They show to calculate tiles in 3d space. and even calculate your mouse in 3d space as well. here is the code. Its alot, but I hope someone else understands it more then I. The book was written by jobe makar on building multiplayer worlds. I wanted to share it because the code it is pretty simple as far as amount of code put into it. only 2 classes needed. I am not that good with trigonometry. so I cant really interpret how the math is getting the results. hopefully someone can explain that for me :D.
Y coordinates are not given because the width is = to height. The coordinates method is just a custom made Point class which holds x, y and z.
package com.gamebook.grid {
import com.gamebook.utils.geom.Coordinate;
import com.gamebook.utils.Isometric;
import flash.display.MovieClip;
import flash.events.MouseEvent;
/**
* ...
* #author Jobe Makar - jobe#electrotank.com
*/
public class Map extends MovieClip{
private var _grid:Array;
private var _iso:Isometric;
private var _tileWidthOnScreen:int;
private var _tileHeightOnScreen:int;
private var _tileWidth:Number;
private var _tileHeight:Number;
private var _cols:int;
private var _rows:int;
private var _lastTile:Tile;
public function Map() {
initialize();
}
private function initialize():void{
_iso = new Isometric();
//when mapped to the screen the tile makes a diamond of these dimensions
_tileWidthOnScreen = 64;
_tileHeightOnScreen = 32;
//figure out the width of the tile in 3D space
_tileWidth = _iso.mapToIsoWorld(64, 0).x;
//the tile is a square in 3D space so the height matches the width
_tileHeight = _tileWidth;
buildGrid();
addEventListener(MouseEvent.MOUSE_MOVE, mouseMoved);
}
private function mouseMoved(e:MouseEvent):void {
if (_lastTile != null) {
_lastTile.alpha = 1;
_lastTile = null;
}
var coord:Coordinate = _iso.mapToIsoWorld(mouseX, mouseY);
var col:int = Math.floor(coord.x / _tileWidth);
var row:int = Math.floor(Math.abs(coord.z / _tileHeight));
if (col < _cols && row < _rows) {
var tile:Tile = getTile(col, row);
tile.alpha = .5;
_lastTile = tile;
}
}
private function buildGrid():void{
_grid = [];
_cols = 10;
_rows = 10;
for (var i:int = 0; i < _cols;++i) {
_grid[i] = [];
for (var j:int = 0; j < _rows;++j) {
var t:Tile = new Tile();
var tx:Number = i * _tileWidth;
var tz:Number = -j * _tileHeight;
var coord:Coordinate = _iso.mapToScreen(tx, 0, tz);
t.x = coord.x;
t.y = coord.y;
_grid[i][j] = t;
addChild(t);
}
}
}
private function getTile(col:int, row:int):Tile {
return _grid[col][row];
}
}
}
Then we have the isometric class that calculates 3d space.
package com.gamebook.utils {
import com.gamebook.utils.geom.Coordinate;
/**
* #author Jobe Makar - jobe#electrotank.com
*/
public class Isometric {
//trigonometric values stored for later use
private var _sinTheta:Number;
private var _cosTheta:Number;
private var _sinAlpha:Number;
private var _cosAlpha:Number;
/**
* Isometric class contrustor.
* #param declination value. Defaults to the most common value, which is 30.
*/
public function Isometric() {
var theta:Number = 30;//even though the tiles are already isometric, you still have to put the degrees the tiles will be turned.
var alpha:Number = 45;//45 degrees on y axis, 30 dgrees on x axis
theta *= Math.PI/180; // then you translate to radians
alpha *= Math.PI/180;
_sinTheta = Math.sin(theta);
_cosTheta = Math.cos(theta);
_sinAlpha = Math.sin(alpha);
_cosAlpha = Math.cos(alpha);
}
/**
* Maps 3D coordinates to the 2D screen
* #param x coordinate
* #param y coordinate
* #param z coordinate
* #return Coordinate instance containig screen x and screen y
*/
public function mapToScreen(xpp:Number, ypp:Number, zpp:Number):Coordinate {
var yp:Number = ypp;
var xp:Number = xpp*_cosAlpha+zpp*_sinAlpha;
var zp:Number = zpp*_cosAlpha-xpp*_sinAlpha;
var x:Number = xp;
var y:Number = yp*_cosTheta-zp*_sinTheta;
return new Coordinate(x, y, 0);
}
/**
* Maps 2D screen coordinates into 3D coordinates. It is assumed that the target 3D y coordinate is 0.
* #param screen x coordinate
* #param screen y coordinate
* #return Coordinate instance containig 3D x, y, and z
*/
public function mapToIsoWorld(screenX:Number, screenY:Number):Coordinate {
var z:Number = (screenX/_cosAlpha-screenY/(_sinAlpha*_sinTheta))*(1/(_cosAlpha/_sinAlpha+_sinAlpha/_cosAlpha));
var x:Number = (1/_cosAlpha)*(screenX-z*_sinAlpha);
return new Coordinate(x, 0, z);
}
}
}