I'm an absolute beginner - that's why I need to ask my question here although I think it's absolutely easily to solve for some of you but: I've not found an answer anywhere online. So heres my "project":
I created a fantasy map using photoshop and now want to use this map to create multiple two-point lines in between different cities. Think of it like a airline destination map, where you can see the departure and arrival airport on a map, and both points connected with a line. Right now, I thought that I can do this by embedding my map .jpg on a html site and then I create a database (with excel) where I specify the name of a city and its coordinates on the image. Afterwards, I thought I can use these data to create those lines between two cities using maybe another database with different origins and destinations on the map. But I now notice, that I need to understand if this all is practicable or a completely wrong approach and how this could be done.
For the two-points on a map connection thingy, I read that there are many solutions for G'maps, but I haven't found a solution for a fantasy map project, yet and am unable to convert those G'map codes to a simple image file.
If you think this can be done and does not need too advanced skills, please let me know.
Anyways, thanks in advance for your help and time,
J.
As this is a 'beginner' project I would suggest using Javascript alongside HTML/CSS. If required, later on setting everything up through a database or other outside source.
The idea in this snippet is to have the map, measure up its width and height (a plastic ruler will do!) and the x, y coordinates of the various places and put the map in the site as an img with a canvas overlaid.
The positioning of the places is made relative to the actual dimensions of the img on the screen, and the places joined by using canvas stroke to draw the lines.
The whole is put inside a function which is called on each resize.
function init() {
const mapW = 300; // width of the map when you measured it (the units you used don't matter as long a you use the same ones for all the measurements)
const mapH = 192; // the height of the map when you measured it (the units you used don't matter)
const places = [{
"name": 'Never Winter Wood',
"x": 125,
"y": 40
}, {
"name": 'Star Mounts',
"x": 165,
"y": 50
}, {
"name": 'Cormanthor',
"x": 275,
"y": 105
}];
const routes = [{
"start": 'Never Winter Wood',
"end": 'Star Mounts'
}, {
"start": 'Cormanthor',
"end": 'Never Winter Wood'
}]
function getPlace(name) {
let result = '';
places.forEach(place => {
if (place.name == name) {
result = place;
}
});
return result;
}
const container = document.querySelector('.container');
const map = document.querySelector('.map');
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const h = map.offsetHeight;
const w = h * mapW / mapH;
canvas.width = w;
canvas.height = h;
container.style.width = w + 'px';
container.style.height = h + 'px';
const scale = w / mapW;
function myMoveTo(place) {
ctx.moveTo(scale * place.x, scale * place.y);
}
function myLineTo(place) {
ctx.lineTo(scale * place.x, scale * place.y);
}
routes.forEach(route => {
const place1 = getPlace(route.start);
const place2 = getPlace(route.end);
ctx.lineWidth = 1;
ctx.beginPath();
myMoveTo(place1);
myLineTo(place2);
ctx.stroke();
});
}
window.onresize = init;
init();
.container {
height: 100vh;
position: relative;
}
.map {
height: 100vh;
width: auto;
position: relative;
display: inline-block;
}
canvas {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
<div class="container">
<img class="map" src="https://i.stack.imgur.com/PGdKQ.jpg">
<canvas></canvas>
</div>
Related
I am trying to add text on screenshot. So my code is:
var system = require('system');
var args = system.args;
var WebPage = require('webpage');
page = WebPage.create();
page.viewportSize = { width: 480, height: 800 };
page.clipRect = { top: 0, left: 0, width: 1024, height: 768 };
page.open(args[1].toString());
page.onLoadFinished = function() {
page.render(args[1] + '.png');
phantom.exit();
}
I want to know how can i modify html content before rendering in order to add some text? I tried to use page.content but unsuccessfully.
Thanks.
You can modify html content with function "page.evaluate". With the function you can run a javascript on the page. Some simple examples can be found at http://phantomjs.org/api/webpage/method/evaluate.html.
Try to add something like the statement below before the render:
page.evaluate(function(str) {
document.querySelector('h2').textContent = str;
}, 'title');
I need to create a setZoom() function in Cesium. For that, I believe I need to evaluate the current zoom so I can decide if I have to use the zoomIn or zoomOut to show what the user asks.
Does anyone know if it's possible to get the zoom level from the map when using Cesium? Or any other solution... Any tips are very welcome.
Does the function getMagnitude() does the trick?
Thanks!
Solution:
I put together all the tips emackey gave to me and got the following code:
var iniPos = new Cesium.Cartesian3();
iniPos = this.viewer.camera.position;
var cartographic = new Cesium.Cartographic();
cartographic.height = zoom * 1000;
cartographic.longitude = iniPos.x;
cartographic.latitude = iniPos.y;
var newPos = new Cesium.Cartesian3();
Cesium.Ellipsoid.WGS84.cartographicToCartesian(cartographic, newPos);
this.viewer.camera.setView({
position: newPos
});
With that I'm able to define the height of the camera to a zoom parameter defined by the user.
Cesium's default view is a 3D globe with a perspective view. A typical 2D zoom level number doesn't fully describe the different resolutions that Cesium's camera can see. Take a minute to read my full answer to Determining the Map Scale of the Viewed Globe for a better explanation.
EDIT 1: The camera.getMagnitude function gets the "magnitude of the camera's position" which really means the distance to the center of the Earth. This is probably not what you want, instead you want the altitude of the cartographic position.
EDIT 2: I've added a code snippet here that has buttons on it to set the camera's height to various altitudes. Click "Run code snippet" at the bottom to see this in action, or copy just the JavaScript portion of this into Sandcastle to run it there. Note that this works best when the camera is looking straight down, as it moves the camera to a specific height without altering the lat/lon. If the camera is off-axis, the mouse can "zoom" the camera along the look vector, which alters all three cartographic coordinates (lat, lon, and alt) at the same time. It's a trickier calculation to move the camera to a specific height along that line, I don't have code for that handy and it might not really be what you want anyway. Give this a try, see if it works for you.
var viewer = new Cesium.Viewer('cesiumContainer', {
navigationHelpButton: false,
animation: false,
timeline: false
});
var cartographic = new Cesium.Cartographic();
var cartesian = new Cesium.Cartesian3();
var camera = viewer.scene.camera;
var ellipsoid = viewer.scene.mapProjection.ellipsoid;
var toolbar = document.getElementById('toolbar');
toolbar.innerHTML = '<div id="hud"></div>' +
'<button type="button" class="cesium-button" id="h1km">1km height</button>' +
'<button type="button" class="cesium-button" id="h10km">10km height</button>' +
'<button type="button" class="cesium-button" id="h500km">500km height</button>';
toolbar.setAttribute('style', 'background: rgba(42,42,42,0.9); border-radius: 5px;');
var hud = document.getElementById('hud');
viewer.clock.onTick.addEventListener(function(clock) {
ellipsoid.cartesianToCartographic(camera.positionWC, cartographic);
hud.innerHTML =
'Lon: ' + Cesium.Math.toDegrees(cartographic.longitude).toFixed(3) + ' deg<br/>' +
'Lat: ' + Cesium.Math.toDegrees(cartographic.latitude).toFixed(3) + ' deg<br/>' +
'Alt: ' + (cartographic.height * 0.001).toFixed(1) + ' km';
});
function setHeightKm(heightInKilometers) {
ellipsoid.cartesianToCartographic(camera.position, cartographic);
cartographic.height = heightInKilometers * 1000; // convert to meters
ellipsoid.cartographicToCartesian(cartographic, cartesian);
camera.position = cartesian;
}
document.getElementById('h1km').addEventListener('click', function() {
setHeightKm(1);
}, false);
document.getElementById('h10km').addEventListener('click', function() {
setHeightKm(10);
}, false);
document.getElementById('h500km').addEventListener('click', function() {
setHeightKm(500);
}, false);
html, body, #cesiumContainer {
width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden;
font-family: sans-serif; color: #edffff;
}
#toolbar {
padding: 2px 5px;
position: absolute;
top: 5px;
left: 5px;
}
<link href="http://cesiumjs.org/Cesium/Build/Cesium/Widgets/widgets.css"
rel="stylesheet"/>
<script src="http://cesiumjs.org/Cesium/Build/Cesium/Cesium.js"></script>
<div id="cesiumContainer"></div>
<div id="toolbar"></div>
I opened a named, singleton window previously with code that set the bounds to { width: 1200, height: 600 }. I then changed my code and updated the app in Chrome but when it opens, it continues to use the previous size! The only way to get it to use a different size is to name it something else. How would I use the same name but change the dimensions?
First version used
var screenWidth = screen.availWidth;
var screenHeight = screen.availHeight;
var width = 1200;
var height = 600;
//Create the app window
chrome.app.window.create(
'options.html',
{
frame: "none",
bounds:
{
width: width,
height: height,
left: Math.round((screenWidth-width)/2),
top: Math.round((screenHeight-height)/2)
},
id: "optionsWindow",
singleton: true
}
);
Updated verions of my app used:
var screenWidth = screen.availWidth;
var screenHeight = screen.availHeight;
var width = 700;
var height = 600;
//Create the app window
chrome.app.window.create(
'options.html',
{
frame: "none",
bounds:
{
width: width,
height: height,
left: Math.round((screenWidth-width)/2),
top: Math.round((screenHeight-height)/2)
},
id: "optionsWindow",
singleton: true
}
);
In both cases, its' 1200 pixels wide. Only if I change the name to "optionsWindow2" does it show up as the new size.
More Info
If I pass a callback to the create function and try to use the AppWindow form there, it says it's null and was not opened by the chrome.app.window! If I create a new, never before created window (e.g. optionsWindow3, then the callback does return a valid AppWindow! Why is this?
To change the size, you must change the ID.
The result you are seeing is by design. If you specify an ID for a window then the bounds you offer are only used once, for the initial window creation. The user's window bounds are then cached and reused when you launch again with the same ID.
I'm having problems with resizing cells and built-in mxGraph layouts.
If I put a cell on canvas, and I try to resize it, even for a pixel, it grows huge, something like 50000px x 30000px, so it streches my whole canvas, and of course it is unusable.
If I load a graph from an xml file from the database, I can resize cells without any problems.
Similar thing happens with the built in layouts. I'd like to use compact tree layout (the reason I like it beacuse it aligns my whole horizontal).
When I draw a graph and try to use that layout, my graph goes wild, also streching to 50000px x 30000 px (example dimensions, but the scroll is so tiny I can barely aim it with the mouse).
If I load a graph from xml from a database, compact tree layout works perfect. But as soon as I add another cell in it, and try to use compact tree layout again, it goes wild, again.
I use absolute positioning for div which holds the canvas, as same as on the example here (http://jgraph.github.io/mxgraph/javascript/examples/editors/workfloweditor.html)
This is my css and html :
<head>
<style type="text/css">
#graphContainer {
background: url('../../resources/jgraph/src/images/grid.gif');
left: 20px;
right: 20px;
top: 65px;
bottom: 20px;
position: absolute;
border: 1px solid #F2F2F2;
white-space: nowrap;
font-family: Arial;
font-size: 8pt;
display: block;
}
</style>
</head>
<body>
<div id="graphContainer"></div>
<script>
$(document).ready(function(){
mc.init(document.getElementById('graphContainer'));
});
</script>
</body>
</html>
And this is my javascript for graph initialization (along with the couple of events, beacuse I'm not sure if they are the problem):
mxConnectionHandler.prototype.connectImage = new mxImage('../../resources/jgraph/src/images/connector.gif', 14, 14);
if (!mxClient.isBrowserSupported()) {
mxUtils.error('Browser is not supported!', 200, false);
} else {
var root = new mxCell();
root.insert(new mxCell());
var model = new mxGraphModel(root);
if (mxClient.IS_QUIRKS)
{
document.body.style.overflow = 'hidden';
new mxDivResizer(graphContainer);
}
var editor = new mxEditor();
editor.setGraphContainer(graphContainer);
editor.readGraphModel(model);
var graph = editor.graph;
graph.setConnectable(true);
new mxRubberband(graph);
/* CODE FOR ADDING THE TOOLBAR, excluded from example */
//code for writing out the node name
graph.convertValueToString = function(cell)
{
if (mxUtils.isNode(cell.value))
{
var outValue = cell.value.getAttribute('nodeName');
if (outValue != null && outValue.length > 0)
{
return outValue;
}
return '';
}
return '';
};
//defining on select event
graph.getSelectionModel().addListener(mxEvent.CHANGE, function(sender, evt)
{
events.cellSelectionChanged(graph, graph.getSelectionCell());
});
//triggering the on select event
events.cellSelectionChanged(graph);
//cells added event
graph.addListener(mxEvent.CELLS_ADDED, function(sender, evt) {
var vertex = evt.getProperties().cells[0];
if(vertex.isVertex()){
var decoder = new mxCodec();
var nodeModel = decoder.decode(vertex.value);
if(nodeModel.type=='node' || nodeModel.type=='branch'){
utils.changeCellAttribute(vertex, 'nodeName', 'Node_' + vertex.id);
}else if(nodeModel.type=='start'){
utils.changeCellAttribute(vertex, 'nodeName', 'START');
}else if(nodeModel.type=='end'){
utils.changeCellAttribute(vertex, 'nodeName', 'END');
}else if(nodeModel.type=='form'){
utils.changeCellAttribute(vertex, 'nodeName', 'Form');
}
}
});
//on connect event
graph.connectionHandler.addListener(mxEvent.CONNECT, function(sender, evt){
var model = graph.getModel();
var edge = evt.getProperty('cell');
var source = model.getTerminal(edge, true);
var target = model.getTerminal(edge, false);
});
}
Any thoughts what the problem might be?
Solution:
Complete graph and cell configuration is loaded from the database (in this example), including the width and height for the cells.
The problem was adding toolbar items for certain cell types, more precise, dropped cell default width and height. As I said we are loading the configuration from the database, it is completely string-ified, so were the width and height.
They both had to be cast to JavaScript Number object for resize and layout to behave properly.
How can I force a new layer added to the map in Leaflet to be the first over the basemap?
I could not find a method to easily change the order of the layers, which is a very basic GIS feature. Am I missing something?
A Leaflet map consists of a collection of "Panes" whose view order is controlled using z-index. Each pane contains a collection of Layers The default pane display order is tiles->shadows->overlays->markers->popups. Like Etienne described, you can control the display order of Paths within the overlays pane by calling bringToFront() or bringToBack(). L.FeatureGroup also has these methods so you can change the order of groups of overlays at once if you need to.
If you want to change the display order of a whole pane then you just change the z-index of the pane using CSS.
If you want to add a new Map pane...well I'm not sure how to do that yet.
http://leafletjs.com/reference.html#map-panes
http://leafletjs.com/reference.html#featuregroup
According to Leaflet API, you can use bringToFront or bringToBack on any layers to brings that layer to the top or bottom of all path layers.
Etienne
For a bit more detail, Bobby Sudekum put together a fantastic demo showing manipulation of pane z-index. I use it as a starting point all the time.
Here's the key code:
var topPane = L.DomUtil.create('div', 'leaflet-top-pane', map.getPanes().mapPane);
var topLayer = L.mapbox.tileLayer('bobbysud.map-3inxc2p4').addTo(map);
topPane.appendChild(topLayer.getContainer());
topLayer.setZIndex(7);
Had to solve this recently, but stumbled upon this question.
Here is a solution that does not rely on CSS hacks and works with layer groups. It essentially removes and re-adds layers in the desired order.
I submit this as a better "best practice" than the current answer. It shows how to manage the layers and re-order them, which is also useful for other contexts. The current method uses the layer Title to identify which layer to re-order, but you can easily modify it to use an index or a reference to the actual layer object.
Improvements, comments, and edits are welcome and encouraged.
JS Fiddle: http://jsfiddle.net/ob1h4uLm/
Or scroll down and click "Run code snippet" and play with it. I set the initial zoom level to a point that should help illustrate the layerGroup overlap effect.
function LeafletHelper() {
// Create the map
var map = L.map('map').setView([39.5, -0.5], 4);
// Set up the OSM layer
var baseLayer = L.tileLayer(
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
var baseLayers = {
"OSM tiles": baseLayer
};
this.map = map;
this.BaseLayers = {
"OSM tiles": baseLayer
};
this.LayersControl = L.control.layers(baseLayers).addTo(map);
this.Overlays = [];
this.AddOverlay = function (layerOptions, markers) {
var zIndex = this.Overlays.length;
var layerGroup = L.layerGroup(markers).addTo(map);
this.LayersControl.addOverlay(layerGroup, layerOptions.title);
this.Overlays.push({
zIndex: zIndex,
LeafletLayer: layerGroup,
Options: layerOptions,
InitialMarkers: markers,
Title: layerOptions.title
});
return layerGroup;
}
this.RemoveOverlays = function () {
for (var i = 0, len = this.Overlays.length; i < len; i++) {
var layer = this.Overlays[i].LeafletLayer;
this.map.removeLayer(layer);
this.LayersControl.removeLayer(layer);
}
this.Overlays = [];
}
this.SetZIndexByTitle = function (title, zIndex) {
var _this = this;
// remove overlays, order them, and re-add in order
var overlays = this.Overlays; // save reference
this.RemoveOverlays();
this.Overlays = overlays; // restore reference
// filter overlays and set zIndex (may be multiple if dup title)
overlays.forEach(function (item, idx, arr) {
if (item.Title === title) {
item.zIndex = zIndex;
}
});
// sort by zIndex ASC
overlays.sort(function (a, b) {
return a.zIndex - b.zIndex;
});
// re-add overlays to map and layers control
overlays.forEach(function (item, idx, arr) {
item.LeafletLayer.addTo(_this.map);
_this.LayersControl.addOverlay(item.LeafletLayer, item.Title);
});
}
}
window.helper = new LeafletHelper();
AddOverlays = function () {
// does not check for dups.. for simple example purposes only
helper.AddOverlay({
title: "Marker A"
}, [L.marker([36.83711, -2.464459]).bindPopup("Marker A")]);
helper.AddOverlay({
title: "Marker B"
}, [L.marker([36.83711, -3.464459]).bindPopup("Marker B")]);
helper.AddOverlay({
title: "Marker C"
}, [L.marker([36.83711, -4.464459]).bindPopup("Marker c")]);
helper.AddOverlay({
title: "Marker D"
}, [L.marker([36.83711, -5.464459]).bindPopup("Marker D")]);
}
AddOverlays();
var z = helper.Overlays.length;
ChangeZIndex = function () {
helper.SetZIndexByTitle(helper.Overlays[0].Title, z++);
}
ChangeZIndexAnim = function () {
StopAnim();
var stuff = ['A', 'B', 'C', 'D'];
var idx = 0;
var ms = 200;
window.tt = setInterval(function () {
var title = "Marker " + stuff[idx++ % stuff.length];
helper.SetZIndexByTitle(title, z++);
}, ms);
}
StopAnim = function () {
if (window.tt) clearInterval(window.tt);
}
#map {
height: 400px;
}
<link rel="stylesheet" type="text/css" href="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.css">
<script type='text/javascript' src="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.js"></script>
<div id="map"></div>
<input type='button' value='Remove overlays' onclick='helper.RemoveOverlays();' />
<input type='button' value='Add overlays' onclick='AddOverlays();' />
<input type='button' value='Move bottom marker to top' onclick='ChangeZIndex();' />
<input type='button' value='Change z Index (Animated)' onclick='ChangeZIndexAnim();' />
<input type='button' value='Stop animation' onclick='StopAnim();' />
I've found this fix (css):
.leaflet-map-pane {
z-index: 2 !important;
}
.leaflet-google-layer {
z-index: 1 !important;
}
found it here: https://gis.stackexchange.com/questions/44598/leaflet-google-map-baselayer-markers-not-visible