Coordinate systems

An introduction to the coordinate system used for placement of spaces and devices.

Currently, we support two different coordinate systems for Spaces and one coordinate system for Devices.

Real coordinates

Real coordinates is only applicable Spaces of type Building, and represent the lat / lon location of the building. This is specified in the GeoJSON coordinates as [latitude, longtitude, altitude], where the altitude is optional and ignored.

Example Building Space
{
  ....
  "type": "SPACE_BUILDING",
  "coordinates": "COORDS_REAL",
  "image": "https://app.neowit.io/api/image/v1/image/<someimage>",
  "features": {
    "type": "Feature",
    "geometry": {
      "type": "Point",
      "coordinates": [
        -74.0088256,
        40.7060361,
        0
      ]
    },
    "properties": {
      "address": "Wall St, New York, NY, USA"
    }
  }
}

Virtual coordinates

The virtual coordinate system is a bit difficult to work with, as it is tightly coupled with our web app and its deck.gl usage. In the future, we will introduce a better coordinate system that is easier to work with and maps better to real word coordinates.

Spaces

All Spaces except Buildings are defined using a GeoJSON Feature using Polygon geometry.

{
    ...
    "name": "My Room",
    "type": "SPACE_ROOM",
    "coordinates": "COORDS_VIRTUAL",
    "image": "https://app.neowit.io/api/image/v1/image/<id>.png",
    "features": {
        "type": "Feature",
        "geometry": {
            "type": "Polygon",
            "coordinates": [
                [
                    [
                        796.5946119869824,
                        908.4491380427061
                    ],
                    [
                        796.5946119869824,
                        717.0977754704593
                    ],
                    [
                        987.9459745592292,
                        717.0977754704593
                    ],
                    [
                        987.9459745592292,
                        908.4491380427061
                    ],
                    [
                        796.5946119869824,
                        908.4491380427061
                    ]
                ]
            ]
        },
        "properties": {
            "shape": "Rectangle"
        }
    }
}

Devices

Devices may be given a position using a GeoJSON Feature using Point geometry. Note that this only makes sense if the Device has an associated Space.

{
    ...
    "name": "A Device",
    "spaceId": "<associatedSpaceId>",
    "features": {
        "type": "Feature",
        "geometry": {
            "type": "Point",
            "coordinates": [
                531.0645562037637,
                11.737089201877893
            ]
        },
        "properties": null
    }
}

Basic workings of the coordinate system

  • 2-dimensional Cartesian coordinate system (x, y) - floating numbers.

  • Bounding box is defined as with a range of (0, 0) - (1000, 1000). Which yields the following coordinates:

   top: left(0, 1000) right(1000, 1000)
bottom: left(0,    0) right(1000,    0)
  • The image uploaded as part of the Floor is preserved in its full size, but we do an object fit into the bounding box, preserving its aspect ratio, when mapping it into the coordinate system. Origo of the object fit is Cartesian (x, y) = (0, 0).

  • Coordinates outside of the [0, 1000] range are permitted for historic reasons.

Converting between image pixel and Cartesian

Example conversion code for TypeScript is show below. This is a small utility class to convert between Cartesian and pixel coordinates defined by the uploaded image.

TypeScript conversion code
type Size = {
	width: number;
	height: number;
};

type Point = {
	x: number;
	y: number;
};

type Domain = {
	max: number;
	min: number;
};

export class Convert {
	private readonly BOX_SIZE = 1000;

	private size: Size;

	private box: Size;

	constructor(size: Size) {
		this.size = size;
		this.box = this.pixelSizeToCartesian(size);
	}

	pixelPointToCartesian(pixel: Point): Point {
		const x = Convert.normalize(
			pixel.x,
			{min: 0, max: this.size.width},
			{min: 0, max: this.box.width},
		);
		const y = Convert.normalize(
			this.size.height - pixel.y,
			{min: 0, max: this.size.height},
			{min: 0, max: this.box.height},
		);
		return {x, y};
	}

	cartesianPointToPixel(coords: Point): Point {
		const x = Convert.normalize(
			coords.x,
			{min: 0, max: this.box.width},
			{min: 0, max: this.size.width},
		);
		const y =
			this.size.height -
			Convert.normalize(
				coords.y,
				{min: 0, max: this.box.height},
				{min: 0, max: this.size.height},
			);
		return {x, y};
	}

	pixelSizeToCartesian(size: Size): Size {
		const ratio = size.height / size.width;
		const width = ratio >= 1 ? this.BOX_SIZE / ratio : this.BOX_SIZE;
		const height = ratio >= 1 ? this.BOX_SIZE : this.BOX_SIZE * ratio;
		return {width, height};
	}

	private static normalize(
		input: number,
		inDomain: Domain,
		outDomain: Domain,
	): number {
		const inputRange = inDomain.max - inDomain.min;
		const outputRange = outDomain.max - outDomain.min;
		return (
			((input - inDomain.min) / inputRange) * outputRange + outDomain.min
		);
	}
}

Example 1 - Image where width > height

// Image size (WxH) = (768x576)
// .----------------------------------------------------.
// | orientation  | image pixel (x,y) | cartesian (x,y) |
// |--------------|-------------------|-----------------|
// | left bottom  |   (  0, 576)      | (   0,   0)     |
// | left top     |   (  0,   0)      | (   0, 750)     |
// | right bottom |   (768, 576)      | (1000,   0)     | 
// | right top    |   (768,   0)      | (1000, 750)     |
// .----------------------------------------------------.
const c = new Convert({width: 768, height: 576});
console.log(
	'left  bottom PIXEL(  0,576) => (   0,   0)',
	c.cartesianPointToPixel({x: 0, y: 0}),
	c.pixelPointToCartesian({x: 0, y: 576}),
);
console.log(
	'left  top    PIXEL(  0,  0) => (   0, 750)',
	c.cartesianPointToPixel({x: 0, y: 750}),
	c.pixelPointToCartesian({x: 0, y: 0}),
);
console.log(
	'right bottom PIXEL(768,576) => (1000,   0)',
	c.cartesianPointToPixel({x: 1000, y: 0}),
	c.pixelPointToCartesian({x: 768, y: 576}),
);
console.log(
	'right top    PIXEL(768,  0) => (1000, 750)',
	c.cartesianPointToPixel({x: 1000, y: 750}),
	c.pixelPointToCartesian({x: 768, y: 0}),
);

Example 2 - Image where width < height

// Image size (WxH) = (576x768)
// .----------------------------------------------------.
// | orientation  | image pixel (x,y) | cartesian (x,y) |
// |--------------|-------------------|-----------------|
// | left bottom  |   (  0, 768)      | (  0,    0)     |
// | left top     |   (  0,   0)      | (  0, 1000)     |
// | right bottom |   (576, 768)      | (750,    0)     |
// | right top    |   (576,   0)      | (750, 1000)     |
// .----------------------------------------------------.
const c = new Convert({width: 576, height: 768});
console.log(
	'left  bottom PIXEL(  0,768) => (  0,    0)',
	c.cartesianPointToPixel({x: 0, y: 0}),
	c.pixelPointToCartesian({x: 0, y: 768}),
);
console.log(
	'left  top    PIXEL(  0,  0) => (  0, 1000)',
	c.cartesianPointToPixel({x: 0, y: 1000}),
	c.pixelPointToCartesian({x: 0, y: 0}),
);
console.log(
	'right bottom PIXEL(576,768) => (750,    0)',
	c.cartesianPointToPixel({x: 750, y: 0}),
	c.pixelPointToCartesian({x: 576, y: 768}),
);
console.log(
	'right top    PIXEL(576,  0) => (750, 1000)',
	c.cartesianPointToPixel({x: 750, y: 1000}),
	c.pixelPointToCartesian({x: 576, y: 0}),
);

Example 3 - Image where width == height

// Image size (WxH) = (768x768)
// .----------------------------------------------------.
// | orientation  | image pixel (x,y) | cartesian (x,y) |
// |--------------|-------------------|-----------------|
// | left bottom  |   (  0, 768)      | (   0,    0)    |
// | left top     |   (  0,   0)      | (   0, 1000)    |
// | right bottom |   (768, 768)      | (1000,    0)    |
// | right top    |   (768,   0)      | (1000, 1000)    |
// .----------------------------------------------------.
const c = new Convert({width: 768, height: 768});
console.log(
	'left  bottom PIXEL(  0,768) => (   0,    0)',
	c.cartesianPointToPixel({x: 0, y: 0}),
	c.pixelPointToCartesian({x: 0, y: 768}),
);
console.log(
	'left  top    PIXEL(  0,  0) => (   0, 1000)',
	c.cartesianPointToPixel({x: 0, y: 1000}),
	c.pixelPointToCartesian({x: 0, y: 0}),
);
console.log(
	'right bottom PIXEL(768,768) => (1000,    0)',
	c.cartesianPointToPixel({x: 1000, y: 0}),
	c.pixelPointToCartesian({x: 768, y: 768}),
);
console.log(
	'right top    PIXEL(768,  0) => (1000, 1000)',
	c.cartesianPointToPixel({x: 1000, y: 1000}),
	c.pixelPointToCartesian({x: 768, y: 0}),
);

Last updated