I'm trying to add a satellite image on my map using OpenLayers 5.
The problem is that I'm not able to do this, because I've just found an option to add an image on the map passing the image extent (xmin, ymin, xmax, ymax) and not the bounding box. The image should fit inside the bounding box. For that reason, the image was distorted.
The image is in JPG file (attribute feature.properties.icon). Example: http://exampleserver.com/220/063/353LGN00/353LGN00_thumb_large.jpg
The result that I would like is something like this:
The result that I've got was that:
My code that adds this image on the map is the following:
import ImageLayer from 'ol/layer/Image'
import Static from 'ol/source/ImageStatic'
...
this.olmap = new Map({
target: 'map',
layers: [
baseLayerGroup, rasterLayerGroup, vectorLayer
],
view: new View({
projection: 'EPSG:4326',
center: [ -45.8392, -3.65286 ],
zoom: 8
})
})
...
this.rasterLayerGroup.getLayers().push(
new ImageLayer({
source: new Static({
url: feature.properties.icon,
projection: 'EPSG:4326',
imageExtent: [
feature.properties.bl_longitude, feature.properties.bl_latitude,
feature.properties.tr_longitude, feature.properties.tr_latitude
]
})
})
)
Would someone know how to pass the image bounding box instead of just the image extent?
Thank you in advance.
EDIT 1: Mike's solution
Through Mike's solution I was able to fix a bug that some images have (near to the equator line). For that reason, his answer solved my problem and it inserted the image in a better position that I was expecting in the moment that I created the question.
However, this solution worked to me with images near to the equator line. Images next to the poles stay distorted (Edit 2).
I send below a picture illustrating the final result:
EDIT 2: New problem?
I was testing some images and I have discovered a new bug. Now I have discovered that the image should fit inside the bounding box. If the image does not fit inside the bbox, it stays distorted, such as the print that I send below illustrating.
The image should fit inside the bbox like in the image below [PS 1]:
I believe that it can be a problem of reprojection, but I don't know, because both view projection and image projection is EPSG:4326.
I tried to follow the explanation about Raster Reprojection[1.] on Openlayers site, however I was not able to reproduce it, because, as I said, both projections (view and image) are the same (or they should be).
I send below the GeoJSON that contains the information related to the image above. The image can be found in "properties.icon" (http://www.dpi.inpe.br/newcatalog/tmp/MOD13Q1/2018/MOD13Q1.A2018017.h13v14.jpg). The bbox coordinates can be found in "geometry.coordinates" or in "properties.bl_latitude", "properties.bl_longitude", "properties.br_latitude" and so on.
"bl" means "bottom left", "br" means "bottom right", "tl" means "top left" and "tr" means "top right". These coordinates inside "properties" are the same inside "geometry.coordinates".
{
"geometry": {
"coordinates": [
[
[
-77.7862,
-50
],
[
-100,
-60
],
[
-80,
-60
],
[
-62.229,
-50
],
[
-77.7862,
-50
]
]
],
"type": "Polygon"
},
"properties": {
"alternate": "http://www.dpi.inpe.br/opensearch/v2/granule.json?uid=MOD13Q1.A2018017.h13v14",
"auxpath": null,
"bitslips": null,
"bl_latitude": -60,
"bl_longitude": -100,
"br_latitude": -60,
"br_longitude": -80,
"centerlatitude": -55,
"centerlongitude": -80.0038,
"centertime": null,
"cloud": 0,
"cloudcovermethod": "M",
"dataset": "MOD13Q1",
"date": "2018-01-17T00:00:00",
"enclosure": [
{
"band": "evi",
"radiometric_processing": "SR",
"type": "MOSAIC",
"url": "http://www.dpi.inpe.br/newcatalog/tmp/MOD13Q1/2018/MOD13Q1.A2018017.h13v14.006.2018033223827.hdf"
},
{
"band": "ndvi",
"radiometric_processing": "SR",
"type": "MOSAIC",
"url": "http://www.dpi.inpe.br/newcatalog/tmp/MOD13Q1/2018/MOD13Q1.A2018017.h13v14.006.2018033223827.hdf"
},
...
],
"icon": "http://www.dpi.inpe.br/newcatalog/tmp/MOD13Q1/2018/MOD13Q1.A2018017.h13v14.jpg",
"id": "http://www.dpi.inpe.br/opensearch/v2/granule.json?uid=MOD13Q1.A2018017.h13v14",
"orbit": 0,
"path": 14,
"provider": "OP_CBERS1",
"row": 13,
"satellite": "T1",
"sensor": "MODIS",
"title": "MOD13Q1.A2018017.h13v14",
"tl_latitude": -50,
"tl_longitude": -77.7862,
"tr_latitude": -50,
"tr_longitude": -62.229,
"type": "IMAGES",
"updated": "2018-03-01T18:51:56",
"via": "http://www.dpi.inpe.br/opensearch/v2/metadata/MOD13Q1.A2018017.h13v14"
},
"type": "Feature"
}
Would someone have a new idea?
[PS 1]: The original code that does the image fits inside the bbox is a Leaflet code [2.] and I send it below:
var map = L.map('map').setView([-15.22, -53.23], 5)
...
var anchor = [
[feature.properties.tl_latitude, feature.properties.tl_longitude],
[feature.properties.tr_latitude, feature.properties.tr_longitude],
[feature.properties.br_latitude, feature.properties.br_longitude],
[feature.properties.bl_latitude, feature.properties.bl_longitude]
]
layer._quicklook = L.imageTransform(feature.properties.icon, anchor).addTo(map)
[1.] https://openlayers.org/en/latest/doc/tutorials/raster-reprojection.html
[2.] https://github.com/ScanEx/Leaflet.imageTransform
If the coordinates are those of the photo and the jpg which contains the rotated photo is in EPSG:4326 (i.e. aligned to meridians and parallels) then you need a bounding box containing all of the corners of the photo
import {boundingExtent} from 'ol/extent';
....
this.rasterLayerGroup.getLayers().push(
new ImageLayer({
source: new Static({
url: feature.properties.icon,
projection: 'EPSG:4326',
imageExtent: boundingExtent([
[feature.properties.bl_longitude, feature.properties.bl_latitude],
[feature.properties.br_longitude, feature.properties.br_latitude],
[feature.properties.tl_longitude, feature.properties.tl_latitude],
[feature.properties.tr_longitude, feature.properties.tr_latitude]
])
})
})
)
However your top screenshot has the jpg itself rotated. If that is desired the projection isn't EPSG:4326 and you would need to define a custom projection to handle the rotation.
I've managed to get something close, but simply stretching the image to fit the polygon doesn't give the exact alignment at the side that the leaflet method does
var properties = {
"bl_latitude": -60,
"bl_longitude": -100,
"br_latitude": -60,
"br_longitude": -80,
"centerlatitude": -55,
"centerlongitude": -80.0038,
"icon": "https://www.mikenunn.net/demo/MOD13Q1.A2018017.h13v14.jpg",
"tl_latitude": -50,
"tl_longitude": -77.7862,
"tr_latitude": -50,
"tr_longitude": -62.229,
};
function overlaySource ( properties ) {
var projection = ol.proj.get('EPSG:3857'); // leaflet projection
var extentSize = [0, 0, 4096, 4096]; // arbitary extent for the projection transforms
var size0 = extentSize[2];
var size1 = extentSize[3];
var url = properties.icon;
var bl = ol.proj.transform([properties.bl_longitude, properties.bl_latitude], 'EPSG:4326', projection);
var tl = ol.proj.transform([properties.tl_longitude, properties.tl_latitude], 'EPSG:4326', projection);
var br = ol.proj.transform([properties.br_longitude, properties.br_latitude], 'EPSG:4326', projection);
var tr = ol.proj.transform([properties.tr_longitude, properties.tr_latitude], 'EPSG:4326', projection);
function normalTransform(coordinates, output, dimensions) {
var dims = dimensions || 2;
for (var i=0; i<coordinates.length; i+=dims) {
var left = bl[0] + (tl[0]-bl[0]) * coordinates[i+1]/size1;
var right = br[0] + (tr[0]-br[0]) * coordinates[i+1]/size1;
var top = tl[1] + (tr[1]-tl[1]) * coordinates[i]/size0;
var bottom = bl[1] + (br[1]-bl[1]) * coordinates[i]/size0;
var newCoordinates0 = left + (right-left) * coordinates[i]/size0;
var newCoordinates1 = bottom + (top-bottom) * coordinates[i+1]/size1;
c = ol.proj.transform([newCoordinates0, newCoordinates1], projection, 'EPSG:3857');
//console.log(coordinates[i] + ' ' + coordinates[i+1] + ' ' + c[0] + ' ' + c[1]);
coordinates[i] = c[0];
coordinates[i+1] = c[1];
}
return coordinates;
}
function rotateTransform(coordinates, output, dimensions) {
var dims = dimensions || 2;
for (var i=0; i<coordinates.length; i+=dims) {
c = ol.proj.transform([coordinates[i], coordinates[i+1]], 'EPSG:3857', projection);
var left = bl[0] + (tl[0]-bl[0]) * (c[1]-bl[1]) /(tl[1]-bl[1]);
var right = br[0] + (tr[0]-br[0]) * (c[1]-br[1]) /(tr[1]-br[1]);
var top = tl[1] + (tr[1]-tl[1]) * (c[0]-tl[0])/(tr[0]-tl[0]);
var bottom = bl[1] + (br[1]-bl[1]) * (c[0]-bl[0])/(br[0]-bl[0]);
var newCoordinates0 = (c[0]-left)*size0/(right-left);
var newCoordinates1 = (c[1]-bottom)*size1/(top-bottom);
//console.log(coordinates[i] + ' ' + coordinates[i+1] + ' ' + newCoordinates0 + ' ' + newCoordinates1);
coordinates[i] = newCoordinates0;
coordinates[i+1] = newCoordinates1;
}
return coordinates;
}
var rotatedProjection = new ol.proj.Projection({
code: 'EPSG:' + url + 'rotated',
units: 'm',
extent: extentSize
});
ol.proj.addProjection(rotatedProjection);
ol.proj.addCoordinateTransforms('EPSG:3857', rotatedProjection,
function(coordinate) {
return rotateTransform(coordinate);
},
function(coordinate) {
return normalTransform(coordinate);
}
);
ol.proj.addCoordinateTransforms('EPSG:4326', rotatedProjection,
function(coordinate) {
return rotateTransform(ol.proj.transform(coordinate, "EPSG:4326", "EPSG:3857"));
},
function(coordinate) {
return ol.proj.transform(normalTransform(coordinate), "EPSG:3857", "EPSG:4326");
}
);
return new ol.source.ImageStatic({
projection: rotatedProjection,
imageExtent: extentSize,
url: url
});
}
var tileLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
attributions: [
'Powered by Esri',
'Source: Esri, DigitalGlobe, GeoEye, Earthstar Geographics, CNES/Airbus DS, USDA, USGS, AeroGRID, IGN, and the GIS User Community'
],
//attributionsCollapsible: false,
url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
maxZoom: 23
})
});
var imageLayer = new ol.layer.Image({
source: overlaySource( properties ),
opacity: 0.7
})
var map = new ol.Map({
layers: [tileLayer, imageLayer],
target: 'map',
logo: false,
view: new ol.View()
});
var imageProj = imageLayer.getSource().getProjection();
map.getView().fit(ol.proj.transformExtent(imageProj.getExtent(), imageProj, map.getView().getProjection()), {constrainResolution: false});
html, body, .map {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
<link href="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/css/ol.css" rel="stylesheet" />
<script src="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/build/ol.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.5.0/proj4.js"></script>
<div id="map" class="map"></div>
This is my KML method, but that is a simple rotation of a rectangle by a specified angle, not warping it into a quadrilateral where only two of the sides are parallel.
function kmlOverlaySource ( kmlExtent, // KMLs specify the extent the unrotated image would occupy
url,
rotation,
imageSize,
) {
// calculate latitude of true scale of equidistant cylindrical projection based on pixels per degree on each axis
proj4.defs('EPSG:' + url, '+proj=eqc +lat_ts=' +
(Math.acos((ol.extent.getHeight(kmlExtent)/imageSize[1])
/(ol.extent.getWidth(kmlExtent)/imageSize[0]))*180/Math.PI) +
' +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs');
if (ol.proj.proj4 && ol.proj.proj4.register) { ol.proj.proj4.register(proj4); } // if OL5 register proj4
// convert the extents to source projection coordinates
var projection = ol.proj.get('EPSG:' + url);
var projExtent = ol.proj.transformExtent(kmlExtent, 'EPSG:4326', projection);
var angle = -rotation * Math.PI/180;
function rotateTransform(coordinates, output, dimensions) {
var dims = dimensions || 2;
for (var i=0; i<coordinates.length; i+=dims) {
var point = new ol.geom.Point([coordinates[i],coordinates[i+1]]);
point.rotate(angle, ol.extent.getCenter(projExtent));
var newCoordinates = point.getCoordinates();
coordinates[i] = newCoordinates[0];
coordinates[i+1] = newCoordinates[1];
}
return coordinates;
}
function normalTransform(coordinates, output, dimensions) {
var dims = dimensions || 2;
for (var i=0; i<coordinates.length; i+=dims) {
var point = new ol.geom.Point([coordinates[i],coordinates[i+1]]);
point.rotate(-angle, ol.extent.getCenter(projExtent));
var newCoordinates = point.getCoordinates();
coordinates[i] = newCoordinates[0];
coordinates[i+1] = newCoordinates[1];
}
return coordinates;
}
var rotatedProjection = new ol.proj.Projection({
code: 'EPSG:' + url + 'rotated',
units: 'm',
extent: projExtent
});
ol.proj.addProjection(rotatedProjection);
ol.proj.addCoordinateTransforms('EPSG:4326', rotatedProjection,
function(coordinate) {
return rotateTransform(ol.proj.transform(coordinate, 'EPSG:4326', projection));
},
function(coordinate) {
return ol.proj.transform(normalTransform(coordinate), projection, 'EPSG:4326');
}
);
ol.proj.addCoordinateTransforms('EPSG:3857', rotatedProjection,
function(coordinate) {
return rotateTransform(ol.proj.transform(coordinate, 'EPSG:3857', projection));
},
function(coordinate) {
return ol.proj.transform(normalTransform(coordinate), projection, 'EPSG:3857');
}
);
return new ol.source.ImageStatic({
projection: rotatedProjection,
url: url,
imageExtent: projExtent
});
}
var tileLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
attributions: [
'Powered by Esri',
'Source: Esri, DigitalGlobe, GeoEye, Earthstar Geographics, CNES/Airbus DS, USDA, USGS, AeroGRID, IGN, and the GIS User Community'
],
//attributionsCollapsible: false,
url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
maxZoom: 23
})
});
// these would normally be parsed from a KML file
var kmlExtent = [8.433995415151397, 46.65804355828784, 9.144871415151389, 46.77980155828784];
var url = 'https://raw.githubusercontent.com/ReneNyffenegger/about-GoogleEarth/master/kml/the_png_says.png'
var rotation = 30;
var imageSize = [];
var img = document.createElement('img');
img.onload = imageLoaded;
img.src = url;
function imageLoaded() {
imageSize[0] = img.width;
imageSize[1] = img.height;
var imageLayer = new ol.layer.Image({
source: kmlOverlaySource(kmlExtent, url, rotation, imageSize),
});
var map = new ol.Map({
layers: [tileLayer, imageLayer],
target: 'map',
logo: false,
view: new ol.View()
});
var imageProj = imageLayer.getSource().getProjection();
map.getView().fit(ol.proj.transformExtent(imageProj.getExtent(), imageProj, map.getView().getProjection()), {constrainResolution: false});
}
html, body, .map {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
<link href="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/css/ol.css" rel="stylesheet" />
<script src="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/build/ol.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.5.0/proj4.js"></script>
<div id="map" class="map"></div>
Related
I have a problem that I can't solve. I want to use JointJS fromJSON function to reconstruct the flowchart from a JSON (previously exported using JoinJS's toJSON function.
The problem is that the call to the fromJSON function always returns the following error:
Whether I call it inside the hook mounted () or call it from the click of a button.
For completeness I also want to say that I am using Vue.js.
The code I'm using instead is the following:
<template>
<div class="wrapper">
<button v-on:click="getGraphJSON">Get graph JSON</button>
<button v-on:click="resetGraphJSON">Restore graph from JSON</button>
<div id="myholder"></div>
</div>
</template>
<script>
const _ = require('lodash')
const joint = require('jointjs')
const g = require('../../node_modules/jointjs/dist/geometry.js')
const backbone = require('../../node_modules/backbone/backbone.js')
const $ = require('../../node_modules/jquery/dist/jquery.js')
import '../../node_modules/jointjs/dist/joint.css';
var CustomRectangle = joint.shapes.standard.Rectangle.define('CustomRectangle', {
type: 'CustomRectangle',
attrs: {
body: {
rx: 10, // add a corner radius
ry: 10,
strokeWidth: 1,
fill: 'cornflowerblue'
},
label: {
textAnchor: 'left', // align text to left
refX: 10, // offset text from right edge of model bbox
fill: 'white',
fontSize: 18
}
}
}, {
markup: [{
tagName: 'rect',
selector: 'body',
}, {
tagName: 'text',
selector: 'label'
}]
}, {
createRandom: function() {
var rectangle = new this();
var fill = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6);
var stroke = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6);
var strokeWidth = Math.floor(Math.random() * 6);
var strokeDasharray = Math.floor(Math.random() * 6) + ' ' + Math.floor(Math.random() * 6);
var radius = Math.floor(Math.random() * 21);
rectangle.attr({
body: {
fill: fill,
stroke: stroke,
strokeWidth: strokeWidth,
strokeDasharray: strokeDasharray,
rx: radius,
ry: radius
},
label: { // ensure visibility on dark backgrounds
fill: 'black',
stroke: 'white',
strokeWidth: 1,
fontWeight: 'bold'
}
});
return rectangle;
}
});
export default {
name: 'JointChartRestorable',
data() {
return {
graph: null,
paper: null,
// graphJSON: JSON.parse('{"cells":[{"type":"standard.Rectangle","position":{"x":100,"y":30},"size":{"width":100,"height":40},"angle":0,"id":"049776c9-7b6d-4aaa-8b02-1edc3bea9852","z":1,"attrs":{"body":{"fill":"blue"},"label":{"fill":"white","text":"Rect #1"}}},{"type":"standard.Rectangle","position":{"x":400,"y":30},"size":{"width":100,"height":40},"angle":0,"id":"b6e77973-1195-4749-99e1-728549329b11","z":2,"attrs":{"body":{"fill":"#2C3E50","rx":5,"ry":5},"label":{"fontSize":18,"fill":"#3498DB","text":"Rect #2","fontWeight":"bold","fontVariant":"small-caps"}}},{"type":"standard.Link","source":{"id":"049776c9-7b6d-4aaa-8b02-1edc3bea9852"},"target":{"id":"b6e77973-1195-4749-99e1-728549329b11"},"id":"4ed8e3b3-55de-4ad2-b79e-d4848adc4a58","labels":[{"attrs":{"text":{"text":"Hello, World!"}}}],"z":3,"attrs":{"line":{"stroke":"blue","strokeWidth":1,"targetMarker":{"d":"M 10 -5 0 0 10 5 Z","stroke":"black","fill":"yellow"},"sourceMarker":{"type":"path","stroke":"black","fill":"red","d":"M 10 -5 0 0 10 5 Z"}}}}],"graphCustomProperty":true,"graphExportTime":1563951791966}')
// graphJSON: JSON.parse('{"cells":[{"type":"examples.CustomRectangle","position":{"x":90,"y":30},"size":{"width":100,"height":40},"angle":0,"id":"faa7f957-4691-4bb2-b907-b2054f7e07de","z":1,"attrs":{"body":{"fill":"blue"},"label":{"text":"Rect #1"}}}]}')
graphJSON: JSON.parse('{"cells":[{"type":"CustomRectangle","position":{"x":100,"y":30},"size":{"width":100,"height":40},"angle":0,"id":"f02da591-c03c-479f-88cf-55c291064ca8","z":1,"attrs":{"body":{"fill":"blue"},"label":{"text":"Rect #1"}}}]}')
};
},
methods: {
getGraphJSON: function() {
this.graphJSON = this.graph.toJSON();
console.log(JSON.stringify(this.graphJSON));
this.graph.get('graphCustomProperty'); // true
this.graph.get('graphExportTime');
},
resetGraphJSON: function() {
if(this.graphJSON !== undefined && this.graphJSON !== null && this.graphJSON !== '') {
this.graph.fromJSON(this.graphJSON);
// this.paper.model.set(this.graphJSON);
} else {
alert('Devi prima cliccare sul tasto "Get graph JSON" almeno una volta');
}
}
},
mounted() {
this.graph = new joint.dia.Graph();
this.graph.fromJSON(this.graphJSON);
// this.graph.set('graphCustomProperty', true);
// this.graph.set('graphExportTime', Date.now());
this.paper = new joint.dia.Paper({
el: document.getElementById('myholder'),
model: this.graph,
width: '100%',
height: 600,
gridSize: 10,
drawGrid: true,
background: {
color: 'rgba(0, 255, 0, 0.3)'
},
// interactive: false, // disable default interaction (e.g. dragging)
/*elementView: joint.dia.ElementView.extend({
pointerdblclick: function(evt, x, y) {
joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments);
this.notify('element:pointerdblclick', evt, x, y);
this.model.remove();
}
}),
linkView: joint.dia.LinkView.extend({
pointerdblclick: function(evt, x, y) {
joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments);
this.notify('link:pointerdblclick', evt, x, y);
this.model.remove();
}
})*/
});
/*this.paper.on('cell:pointerdblclick', function(cellView) {
var isElement = cellView.model.isElement();
var message = (isElement ? 'Element' : 'Link') + ' removed';
eventOutputLink.attr('label/text', message);
eventOutputLink.attr('body/visibility', 'visible');
eventOutputLink.attr('label/visibility', 'visible');
});*/
/***************************************************/
/************** GRAPH ELEMENT SAMPLE ***************/
/***************************************************/
// var rect = new joint.shapes.standard.Rectangle();
// var rect = new CustomRectangle();
// rect.position(100, 30);
// rect.resize(100, 40);
// rect.attr({
// body: {
// fill: 'blue'
// },
// label: {
// text: 'Rect #1',
// fill: 'white'
// }
// });
// rect.addTo(this.graph);
/***************************************************/
/************** GRAPH ELEMENT SAMPLE ***************/
/***************************************************/
}
}
</script>
Right now I'm using a custom element, previously defined, but I've also done tests using the standard Rectangle element of JointJS.
Can anyone tell me if I'm doing something wrong?
Many thanks in advance.
Markup object could not be found in element that's reason why this error is getting. After it's imported jointjs to the vueJS project through jointjs or rabbit dependency;
import * as joint from 'jointjs' or import * as joint from 'rabbit'
window.joint = joint;
joint should be adjusted as global in environment by using window.
I am trying to explore cesium 3d tileset. but I did not find a way to position tileset.
I tried transformation and bounding volumes in tileset.json but that wont work.
{
"asset": {
"version": "1.0"
},
"properties": {
"Height": {
"minimum": 20,
"maximum": 20
}
},
"geometricError": 70,
"root": {
"refine": "ADD",
"transform": [
1.0001,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
],
"boundingVolume": {
"region": [
-1.3197004795898053,
0.6988582109,
-1.3196595204101946,
0.6988897891,
0,
30
]
},
"geometricError": 0,
"content": {
"uri": "instancedOrientation.i3dm"
}
}
}
I found how to position tileset in cesium.
Just change translation in transform matrix
tileset = cviewer.scene.primitives.add(new Cesium.Cesium3DTileset({
url : url
}));
//Original location of tileset
let pos = {
x: 1214939.1184933728,
y: -4736505.884059093,
z: 4081598.457967,
};
tileset._root.transform[12] = cviewer.camera.position.x - pos.x;
tileset._root.transform[13] = cviewer.camera.position.y - pos.y;
tileset._root.transform[14] = cviewer.camera.position.z - pos.z;
I recently had to do this but I also needed a rotation and a scale.
Here's a demo
And the code
It seems that today, the correct way to transform a tileset in cesium is through the tileset.modelMatrix
Here's the function that calculates the transformation matrix given lon/lat/height coordinates, a rotation and a scale with the correct matrix multiplication order:
function computeTransformationMatrix(lon, lat, height, rotation, scale) {
const location = Cesium.Cartesian3.fromDegrees(lon, lat, height);
const translationMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(location);
const scaleMatrix = Cesium.Matrix4.fromScale(scale)
const rotationMatrixX = Cesium.Matrix3.fromRotationX(rotation.x);
const rotationMatrixY = Cesium.Matrix3.fromRotationY(rotation.y);
const rotationMatrixZ = Cesium.Matrix3.fromRotationZ(rotation.z);
const rotation3 = new Cesium.Matrix3();
Cesium.Matrix3.multiply(rotationMatrixZ, rotationMatrixY, rotation3);
Cesium.Matrix3.multiply(rotationMatrixX, rotation3, rotation3);
const rotationMatrix = Cesium.Matrix4.fromRotation(rotation3);
const matrix = new Cesium.Matrix4();
Cesium.Matrix4.multiply(translationMatrix, scaleMatrix, matrix);
Cesium.Matrix4.multiply(matrix, rotationMatrix, matrix);
return matrix;
}
I have a requirement to render a set of time series data of contiguous blocks.
I need to describe a series of bars which could span many hours, or just minutes, with their own Y value.
I'm not sure if ChartJS is what I should be using for this, but I have looked at extending the Bar type, but it seems very hard coded for each bar to be the same width. The Scale Class internally is used for labels, chart width etc, not just the bars themselves.
I am trying to achieve something like this that works in Excel: http://peltiertech.com/variable-width-column-charts/
Has anyone else had to come up with something similar?
I found I needed to do this and the answer by #potatopeelings was great, but out of date for version 2 of Chartjs. I did something similar by creating my own controller/chart type via extending bar:
//controller.barw.js
module.exports = function(Chart) {
var helpers = Chart.helpers;
Chart.defaults.barw = {
hover: {
mode: 'label'
},
scales: {
xAxes: [{
type: 'category',
// Specific to Bar Controller
categoryPercentage: 0.8,
barPercentage: 0.9,
// grid line settings
gridLines: {
offsetGridLines: true
}
}],
yAxes: [{
type: 'linear'
}]
}
};
Chart.controllers.barw = Chart.controllers.bar.extend({
/**
* #private
*/
getRuler: function() {
var me = this;
var scale = me.getIndexScale();
var options = scale.options;
var stackCount = me.getStackCount();
var fullSize = scale.isHorizontal()? scale.width : scale.height;
var tickSize = fullSize / scale.ticks.length;
var categorySize = tickSize * options.categoryPercentage;
var fullBarSize = categorySize / stackCount;
var barSize = fullBarSize * options.barPercentage;
barSize = Math.min(
helpers.getValueOrDefault(options.barThickness, barSize),
helpers.getValueOrDefault(options.maxBarThickness, Infinity));
return {
fullSize: fullSize,
stackCount: stackCount,
tickSize: tickSize,
categorySize: categorySize,
categorySpacing: tickSize - categorySize,
fullBarSize: fullBarSize,
barSize: barSize,
barSpacing: fullBarSize - barSize,
scale: scale
};
},
/**
* #private
*/
calculateBarIndexPixels: function(datasetIndex, index, ruler) {
var me = this;
var scale = ruler.scale;
var options = scale.options;
var isCombo = me.chart.isCombo;
var stackIndex = me.getStackIndex(datasetIndex);
var base = scale.getPixelForValue(null, index, datasetIndex, isCombo);
var size = ruler.barSize;
var dataset = me.chart.data.datasets[datasetIndex];
if(dataset.weights) {
var total = dataset.weights.reduce((m, x) => m + x, 0);
var perc = dataset.weights[index] / total;
var offset = 0;
for(var i = 0; i < index; i++) {
offset += dataset.weights[i] / total;
}
var pixelOffset = Math.round(ruler.fullSize * offset);
var base = scale.isHorizontal() ? scale.left : scale.top;
base += pixelOffset;
size = Math.round(ruler.fullSize * perc);
size -= ruler.categorySpacing;
size -= ruler.barSpacing;
}
base -= isCombo? ruler.tickSize / 2 : 0;
base += ruler.fullBarSize * stackIndex;
base += ruler.categorySpacing / 2;
base += ruler.barSpacing / 2;
return {
size: size,
base: base,
head: base + size,
center: base + size / 2
};
},
});
};
Then you need to add it to your chartjs instance like this:
import Chart from 'chart.js'
import barw from 'controller.barw'
barw(Chart); //add plugin to chartjs
and finally, similar to the other answer, the weights of the bar widths need to be added to the data set:
var data = {
labels: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
datasets: [
{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.5)",
strokeColor: "rgba(220,220,220,0.8)",
highlightFill: "rgba(220,220,220,0.7)",
highlightStroke: "rgba(220,220,220,1)",
data: [65, 59, 80, 30, 56, 65, 40],
weights: [1, 0.9, 1, 2, 1, 4, 0.3]
},
]
};
This will hopefully get someone onto the right track. What I have certainly isn't perfect, but if you make sure you have the right number of weight to data points, you should be right.
Best of luck.
This is based on the #Shane's code, I just posted to help, since is a common question.
calculateBarIndexPixels: function (datasetIndex, index, ruler) {
const options = ruler.scale.options;
const range = options.barThickness === 'flex' ? computeFlexCategoryTraits(index, ruler, options) : computeFitCategoryTraits(index, ruler, options);
const barSize = range.chunk;
const stackIndex = this.getStackIndex(datasetIndex, this.getMeta().stack);
let center = range.start + range.chunk * stackIndex + range.chunk / 2;
let size = range.chunk * range.ratio;
let start = range.start;
const dataset = this.chart.data.datasets[datasetIndex];
if (dataset.weights) {
//the max weight should be one
size = barSize * dataset.weights[index];
const meta = this.chart.controller.getDatasetMeta(0);
const lastModel = index > 0 ? meta.data[index - 1]._model : null;
//last column takes the full bar
if (lastModel) {
//start could be last center plus half of last column width
start = lastModel.x + lastModel.width / 2;
}
center = start + size * stackIndex + size / 2;
}
return {
size: size,
base: center - size / 2,
head: center + size / 2,
center: center
};
}
For Chart.js you can create a new extension based on the bar class to do this. It's a bit involved though - however most of it is a copy paste of the bar type library code
Chart.types.Bar.extend({
name: "BarAlt",
// all blocks that don't have a comment are a direct copy paste of the Chart.js library code
initialize: function (data) {
// the sum of all widths
var widthSum = data.datasets[0].data2.reduce(function (a, b) { return a + b }, 0);
// cumulative sum of all preceding widths
var cumulativeSum = [ 0 ];
data.datasets[0].data2.forEach(function (e, i, arr) {
cumulativeSum.push(cumulativeSum[i] + e);
})
var options = this.options;
// completely rewrite this class to calculate the x position and bar width's based on data2
this.ScaleClass = Chart.Scale.extend({
offsetGridLines: true,
calculateBarX: function (barIndex) {
var xSpan = this.width - this.xScalePaddingLeft;
var x = this.xScalePaddingLeft + (cumulativeSum[barIndex] / widthSum * xSpan) - this.calculateBarWidth(barIndex) / 2;
return x + this.calculateBarWidth(barIndex);
},
calculateBarWidth: function (index) {
var xSpan = this.width - this.xScalePaddingLeft;
return (xSpan * data.datasets[0].data2[index] / widthSum);
}
});
this.datasets = [];
if (this.options.showTooltips) {
Chart.helpers.bindEvents(this, this.options.tooltipEvents, function (evt) {
var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : [];
this.eachBars(function (bar) {
bar.restore(['fillColor', 'strokeColor']);
});
Chart.helpers.each(activeBars, function (activeBar) {
activeBar.fillColor = activeBar.highlightFill;
activeBar.strokeColor = activeBar.highlightStroke;
});
this.showTooltip(activeBars);
});
}
this.BarClass = Chart.Rectangle.extend({
strokeWidth: this.options.barStrokeWidth,
showStroke: this.options.barShowStroke,
ctx: this.chart.ctx
});
Chart.helpers.each(data.datasets, function (dataset, datasetIndex) {
var datasetObject = {
label: dataset.label || null,
fillColor: dataset.fillColor,
strokeColor: dataset.strokeColor,
bars: []
};
this.datasets.push(datasetObject);
Chart.helpers.each(dataset.data, function (dataPoint, index) {
datasetObject.bars.push(new this.BarClass({
value: dataPoint,
label: data.labels[index],
datasetLabel: dataset.label,
strokeColor: dataset.strokeColor,
fillColor: dataset.fillColor,
highlightFill: dataset.highlightFill || dataset.fillColor,
highlightStroke: dataset.highlightStroke || dataset.strokeColor
}));
}, this);
}, this);
this.buildScale(data.labels);
// remove the labels - they won't be positioned correctly anyway
this.scale.xLabels.forEach(function (e, i, arr) {
arr[i] = '';
})
this.BarClass.prototype.base = this.scale.endPoint;
this.eachBars(function (bar, index, datasetIndex) {
// change the way the x and width functions are called
Chart.helpers.extend(bar, {
width: this.scale.calculateBarWidth(index),
x: this.scale.calculateBarX(index),
y: this.scale.endPoint
});
bar.save();
}, this);
this.render();
},
draw: function (ease) {
var easingDecimal = ease || 1;
this.clear();
var ctx = this.chart.ctx;
this.scale.draw(1);
Chart.helpers.each(this.datasets, function (dataset, datasetIndex) {
Chart.helpers.each(dataset.bars, function (bar, index) {
if (bar.hasValue()) {
bar.base = this.scale.endPoint;
// change the way the x and width functions are called
bar.transition({
x: this.scale.calculateBarX(index),
y: this.scale.calculateY(bar.value),
width: this.scale.calculateBarWidth(index)
}, easingDecimal).draw();
}
}, this);
}, this);
}
});
You pass in the widths like below
var data = {
labels: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
datasets: [
{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.5)",
strokeColor: "rgba(220,220,220,0.8)",
highlightFill: "rgba(220,220,220,0.7)",
highlightStroke: "rgba(220,220,220,1)",
data: [65, 59, 80, 30, 56, 65, 40],
data2: [10, 20, 30, 20, 10, 40, 10]
},
]
};
and you call it like so
var ctx = document.getElementById('canvas').getContext('2d');
var myLineChart = new Chart(ctx).BarAlt(data);
Fiddle - http://jsfiddle.net/moye0cp4/
I wanted to move the model dynamically using keyboard shortcuts. I could not find relevant article on that.
So for now, I'm trying to move the model on click. When click on the model. The model has to move in one direction (increment the value 1 on tick). Find below the sandcastle code for that.
var selectedMesh; var i=0;
var viewer = new Cesium.Viewer('cesiumContainer', {
infoBox: false,
selectionIndicator: false
});
var handle = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
function createModel(url, height) {
viewer.entities.removeAll();
var position = Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706, height);
var heading = Cesium.Math.toRadians(135);
var pitch = 0;
var roll = 0;
var orientation = Cesium.Transforms.headingPitchRollQuaternion(position, heading, pitch, roll);
var entity = viewer.entities.add({
name: url,
position: position,
orientation: orientation,
model: {
uri: url,
minimumPixelSize: 128
}
});
viewer.trackedEntity = entity;
viewer.clock.onTick.addEventListener(function () {
if (selectedMesh) {
console.log("Before 0 : " + selectedMesh.primitive.modelMatrix[12]);
selectedMesh.primitive.modelMatrix[12] = selectedMesh.primitive.modelMatrix[12] + 1;
console.log("After 0 : " + selectedMesh.primitive.modelMatrix[12]);
}
});
}
handle.setInputAction(function (movement) {
console.log("LEFT CLICK");
var pick = viewer.scene.pick(movement.position);
if (Cesium.defined(pick) && Cesium.defined(pick.node) && Cesium.defined(pick.mesh)) {
if (!selectedMesh) {
selectedMesh = pick;
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
var options = [{
text: 'Aircraft',
onselect: function () {
createModel('../../SampleData/models/CesiumAir/Cesium_Air.bgltf', 5000.0);
}
}, {
text: 'Ground vehicle',
onselect: function () {
createModel('../../SampleData/models/CesiumGround/Cesium_Ground.bgltf', 0);
}
}, {
text: 'Milk truck',
onselect: function () {
createModel('../../SampleData/models/CesiumMilkTruck/CesiumMilkTruck.bgltf', 0);
}
}, {
text: 'Skinned character',
onselect: function () {
createModel('../../SampleData/models/CesiumMan/Cesium_Man.bgltf', 0);
}
}];
Sandcastle.addToolbarMenu(options);
When I click, the model is moving for the first time. After that, It stays on the same place. I've printed the value in the console. It seems the value is not changing. I'm not sure about the problem here. or I'm implementing the transformation wrongly.
If you keep track of the current lat and lon of the entity, and adjust that lat and lon based on user input, all you need to do is update the orientation of the entity.
var lon = // the updated lon
var lat = // updated lat
var position = Cesium.Cartesian3.fromDegrees(lon, lat, height);
var heading = Cesium.Math.toRadians(135);
var pitch = 0;
var roll = 0;
// create an orientation based on the new position
var orientation = Cesium.Transforms.headingPitchRollQuaternion(position, heading, pitch, roll);
Then you just need to update the orientation of the entity.
entity.orientation = orientation;
By changing the value, the models orientation, and therefore position will get updated.
I'm stuck. I can't get my checkbox functional. I would like to toggle two layers with a checkbox. The two layers should be on when loaded (check box checked) and then turned off when unchecked. Simple, I know, but I can't get it functional despite looking at lots of working examples and checking all the posts. Most of the stuff I found were how it worked with KML and fusion tables. I have something working kinda that works for a button but this doesn't work well for check boxes.
Here is the code for the drop down box with the check boxes.
tileNEX = new google.maps.ImageMapType({
getTileUrl: function(tile, zoom) {
return "http://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913/" + zoom + "/" + tile.x + "/" + tile.y +".png?"+ (new Date()).getTime();
},
tileSize: new google.maps.Size(256, 256),
opacity:0.50,
name : 'NEXRAD',
isPng: true
});
goes = new google.maps.ImageMapType({
getTileUrl: function(tile, zoom) {
return "http://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/goes-ir-4km-900913/" + zoom + "/" + tile.x + "/" + tile.y +".png?"+ (new Date()).getTime();
},
tileSize: new google.maps.Size(256, 256),
opacity:0.50,
name : 'GOES NA Vis',
isPng: true
});
//create the check box items
var radarOptions = {
gmap: map,
title: "This allows for selection/toggling on/off Radar layer",
id: "radarCheck",
label: "Radar",
action: function(){
if (map.overlayMapTypes.length==0) {
map.overlayMapTypes.push(null); // create empty overlay entry
map.overlayMapTypes.setAt("1",tileNEX);
}
else {
map.overlayMapTypes.clear();
}
}
}
var check1 = new checkBox(radarOptions);
var radarAnimate = {
gmap: map,
title: "This allows for selection/toggling on/off Animated Radar",
id: "radarAnimate",
label: "Animate Radar",
action: function(){
alert('Function is coming soon');
}
}
var check2 = new checkBox(radarAnimate);
var satOptions = {
gmap: map,
title: "This allows for selection/toggling on/off Satelite layer",
id: "satCheck",
label: "Satellite",
action: function(){
if (map.overlayMapTypes.length==0) {
map.overlayMapTypes.push(null); // create empty overlay entry
map.overlayMapTypes.setAt("1",goes);
}
else {
map.overlayMapTypes.clear();
}
}
}
var check3 = new checkBox(satOptions);
//create the input box items
//possibly add a separator between controls
var sep = new separator();
//put them all together to create the drop down
var ddDivOptions = {
items: [sep, check1, check2, check3],
id: "myddOptsDiv"
}
//alert(ddDivOptions.items[1]);
var dropDownDiv = new dropDownOptionsDiv(ddDivOptions);
var dropDownOptions = {
gmap: map,
name: 'Options',
id: 'ddControl',
title: 'A custom drop down select with mixed elements',
position: google.maps.ControlPosition.TOP_RIGHT,
dropDown: dropDownDiv
}
var dropDown1 = new dropDownControl(dropDownOptions);
FULL CODE
LINK TO MAP