Google Maps API v3: data layer IDs undefined - javascript

long-time lurker, first-time poster here, so be gentle...
I'm building a map that uses data parsed in from a MYSQL db via PHP to set the colour of polygons defined by a geoJson file (it uses this example on the Google dev site as a template). The problem I'm having is that the data layer won't automatically initialize when the page loads.
The full javascript/HTML is posted below, but the section of code that was used in the example I'm following to initialize the data layer is:
google.maps.event.addListenerOnce(map.data, 'addfeature', function() {
google.maps.event.trigger(document.getElementById('price_select'),
'change');
});
This gives me the error "Uncaught TypeError: Cannot read property 'setProperty' of undefined". If I comment out the listener, the data layer will load fine, but only after I've manually selected a new input from the drop down (id='price_select').
The geoJson file I'm loading is relatively large (~14mb) so I think what is happening is that the listener is triggering before the whole file is loaded ('addfeature' waits for just the first feature to be added, but I have >2000) and thus the districts parsed in by the PHP don't yet have a corresponding feature ID, which is set by the idPropertyName: 'Name' parameter in the loadGeoJson call. I don't know of a way to set the listener to trigger only once the whole GeoJson file has loaded though. Alternatively, I could be completely wrong about this being the cause of the error.
Anyway, full code below - I know it has some quirks (e.g. I pass the loadData function an argument, but don't use it), but these are mainly because I plan to add more functionality later on. Thanks for bearing with me!
<script src="https://maps.googleapis.com/maps/api/js?v=3.exp"></script>
<script>
var map;
var priceMin = 1000000;
var priceMax = 0;
google.maps.event.addDomListener(window, 'load', function(){
map = new google.maps.Map(document.getElementById('map-canvas'), {
center: new google.maps.LatLng(53.587425,-1.663539),
zoom: 7,
});
// add styles
map.data.setStyle(styleFeature);
map.data.addListener('mouseover', mouseInToRegion);
map.data.addListener('mouseout', mouseOutOfRegion);
// initiate drop down functionality
var selectBox = document.getElementById('price_select');
google.maps.event.addDomListener(selectBox, 'change', function() {
clearData();
loadData(selectBox.options[selectBox.selectedIndex].value);
});
// load polygons
loadMapShapes();
});
function loadMapShapes(){
map.data.loadGeoJson('http://localhost/OS_raw.json',
{ idPropertyName: 'Name' });
//This is the listener that is supposed to initiate the data layer
google.maps.event.addListenerOnce(map.data, 'addfeature', function() {
google.maps.event.trigger(document.getElementById('price_select'),
'change');
});
// End listener
}
function loadData(variable){
var phpdistricts = (<?php echo $phpdistricts; ?>);
var phpprices = (<?php echo $phpprices; ?>);
for(var i=0; i<phpdistricts.length; i++){
var district = phpdistricts[i];
var price = parseInt(phpprices[i]);
// keep track of min and max values
if (price < priceMin) {
priceMin = price;
}
if (price > priceMax) {
priceMax = price;
}
console.log(map.data.getFeatureById(district));
//This is where the error triggers - feature is undefined according to console log above
map.data
.getFeatureById(district)
.setProperty('price', price);}
//end of problematic section
// update and display the legend
document.getElementById('census-min').textContent =
priceMin.toLocaleString();
document.getElementById('census-max').textContent =
priceMax.toLocaleString();
}
function clearData() {
priceMin = 1000000;
priceMax = 0;
map.data.forEach(function(row) {
row.setProperty('price', undefined);
});
document.getElementById('data-box').style.display = 'none';
document.getElementById('data-caret').style.display = 'none';
}
function styleFeature(feature) {
var low = [151, 83, 34]; // color of smallest datum
var high = [5, 69, 54]; // color of largest datum
// delta represents where the value sits between the min and max
var delta = (feature.getProperty('price') - priceMin) /
(priceMax - priceMin);
var color = [];
for (var i = 0; i < 3; i++) {
// calculate an integer color based on the delta
color[i] = (high[i] - low[i]) * delta + low[i];
}
// filters out areas without data
var showRow = true;
if (feature.getProperty('price') == null ||
isNaN(feature.getProperty('price'))) {
showRow = false;
}
var outlineWeight = 0.5, zIndex = 1;
if (feature.getProperty('state') === 'hover') {
outlineWeight = zIndex = 2;
}
return {
strokeWeight: outlineWeight,
strokeColor: '#fff',
zIndex: zIndex,
fillColor: 'hsl(' + color[0] + ',' + color[1] + '%,' + color[2] + '%)',
fillOpacity: 0.75,
visible: showRow
};
}
function mouseInToRegion(e) {
// set the hover state so the setStyle function can change the border
e.feature.setProperty('state', 'hover');
var percent = (e.feature.getProperty('price') - priceMin) /
(priceMax - priceMin) * 100;
// update the label
document.getElementById('data-label').textContent =
e.feature.getProperty('Name');
document.getElementById('data-value').textContent =
e.feature.getProperty('price');
document.getElementById('data-box').style.display = 'block';
document.getElementById('data-caret').style.display = 'block';
document.getElementById('data-caret').style.paddingLeft = percent + '%';
}
function mouseOutOfRegion(e) {
// reset the hover state, returning the border to normal
e.feature.setProperty('state', 'normal');
}
</script>
</head>
<body>
<div id="controls" class="nicebox">
<div>
<select id="price_select">
<option value="price">Jun '14</option>
<option value="price">Jun '14</option>
</select>
</div>
<div id="legend">
<div id="census-min">min</div>
<div class="color-key">
<span id="data-caret">◆</span>
</div>
<div id="census-max">max</div>
</div>
</div>
<div id="data-box" class="nicebox">
<label id="data-label" for="data-value">Area: </label>
<span id="data-value"></span>
</div>
<div id="map-canvas"></div>
</body>
</html>

I don't fully get what you're asking here, but have you tried styling using the data layer events, rather than the maps?
map.data.setStyle(
function(feature){
// Build your styles here based on feature properties.
return style_i_want_for_this_feature;
}
);
That will get called for each feature in your GeoJSON data, and you can treat it appropriately. I load and style feature collections with thousands of features this way.

Related

Angular - Can Leaflet measurement tool mute a layer's interactivity?

I'm using Leaflet.Measure for measuring areas and distances and I also have several layers on a map. When you click on a layer, the onclick function is being fired and a popup appears. When I start to measure a new area and click on a map layer, the popup appears again and I cannot measure area further. Is there any possibility to mute layer's interactivity, while I'm using measurement tool?
I tired to import my Angular service class into a measuring plugin to change layer's onclick behaviour, but it didn't work.
Here's a part of Leaflet.Measure plugin code, which I have modified:
import { MapService} from './app/services/map.service';
L.MeasureAction = L.Handler.extend({
_onMouseClick: function (event) {
MapService.enablePopupOpen = false; // if it's set to false, then popup won't open
//default plugin code
var latlng = event.latlng || this._map.mouseEventToLatLng(event);
if (this._lastPoint && latlng.equals(this._lastPoint)) {
return;
}
if (this._trail.points.length > 0) {
var points = this._trail.points;
points.push(latlng);
var length = points.length;
this._totalDistance += this._getDistance(points[length - 2], points[length - 1]);
this._addMeasurePoint(latlng);
//this._addMarker(latlng);
if (this.options.model !== "area") {
this._addLable(latlng, this._getDistanceString(this._totalDistance), "leaflet-measure-lable");
}
} else {
this._totalDistance = 0;
this._addMeasurePoint(latlng);
this._addMarker(latlng);
if (this.options.model !== "area") {
this._addLable(latlng, L.Measure.start, "leaflet-measure-lable");
}
this._trail.points.push(latlng);
}
this._lastPoint = latlng;
this._startMove = false;
},
...
})

How to retain drawn boxes and path on page refresh mxgraph

i want to retain drawn boxes and path once page is refreshed.
i'm using mxgraph https://jgraph.github.io/mxgraph/docs/manual.html
Question: once the drawing is done, if i do page refresh it should retain the same drawing by storing drawing data into localStorage.
NOTE: i want to get json object of drawn canvas, and re-construct it back from that object on page refresh.
Below video shows how to draw: https://drive.google.com/file/d/1McTMz3e8I_quOcLGt2CtaQhqif3R31qs/view
https://jgraph.github.io/mxgraph/javascript/examples/editors/images/
<!--
Copyright (c) 2006-2013, JGraph Ltd
Dynamic toolbar example for mxGraph. This example demonstrates changing the
state of the toolbar at runtime.
-->
<html>
<head>
<title>Toolbar example for mxGraph</title>
<!-- Sets the basepath for the library if not in same directory -->
<script type="text/javascript">
mxBasePath = 'https://jgraph.github.io/mxgraph/javascript/src';
</script>
<!-- Loads and initializes the library -->
<script type="text/javascript" src="https://jgraph.github.io/mxgraph/javascript/src/js/mxClient.js"></script>
<!-- Example code -->
<script type="text/javascript">
// Program starts here. Creates a sample graph in the
// DOM node with the specified ID. This function is invoked
// from the onLoad event handler of the document (see below).
function main()
{
// Checks if browser is supported
if (!mxClient.isBrowserSupported())
{
// Displays an error message if the browser is
// not supported.
mxUtils.error('Browser is not supported!', 200, false);
}
else
{
// Defines an icon for creating new connections in the connection handler.
// This will automatically disable the highlighting of the source vertex.
mxConnectionHandler.prototype.connectImage = new mxImage('images/connector.gif', 16, 16);
// Creates the div for the toolbar
var tbContainer = document.createElement('div');
tbContainer.style.position = 'absolute';
tbContainer.style.overflow = 'hidden';
tbContainer.style.padding = '2px';
tbContainer.style.left = '0px';
tbContainer.style.top = '0px';
tbContainer.style.width = '24px';
tbContainer.style.bottom = '0px';
document.body.appendChild(tbContainer);
// Creates new toolbar without event processing
var toolbar = new mxToolbar(tbContainer);
toolbar.enabled = false
// Creates the div for the graph
var container = document.createElement('div');
container.style.position = 'absolute';
container.style.overflow = 'hidden';
container.style.left = '24px';
container.style.top = '0px';
container.style.right = '0px';
container.style.bottom = '0px';
container.style.background = 'url("editors/images/grid.gif")';
document.body.appendChild(container);
// Workaround for Internet Explorer ignoring certain styles
if (mxClient.IS_QUIRKS)
{
document.body.style.overflow = 'hidden';
new mxDivResizer(tbContainer);
new mxDivResizer(container);
}
// Creates the model and the graph inside the container
// using the fastest rendering available on the browser
var model = new mxGraphModel();
var graph = new mxGraph(container, model);
// Enables new connections in the graph
graph.setConnectable(true);
graph.setMultigraph(false);
// Stops editing on enter or escape keypress
var keyHandler = new mxKeyHandler(graph);
var rubberband = new mxRubberband(graph);
var addVertex = function(icon, w, h, style)
{
var vertex = new mxCell(null, new mxGeometry(0, 0, w, h), style);
vertex.setVertex(true);
var img = addToolbarItem(graph, toolbar, vertex, icon);
img.enabled = true;
graph.getSelectionModel().addListener(mxEvent.CHANGE, function()
{
var tmp = graph.isSelectionEmpty();
mxUtils.setOpacity(img, (tmp) ? 100 : 20);
img.enabled = tmp;
});
};
addVertex('https://jgraph.github.io/mxgraph/javascript/examples/editors/images/rectangle.gif', 100, 40, '');
addVertex('https://jgraph.github.io/mxgraph/javascript/examples/editors/images/rounded.gif', 100, 40, 'shape=rounded');
addVertex('https://jgraph.github.io/mxgraph/javascript/examples/editors/images/ellipse.gif', 40, 40, 'shape=ellipse');
addVertex('https://jgraph.github.io/mxgraph/javascript/examples/editors/images/rhombus.gif', 40, 40, 'shape=rhombus');
addVertex('https://jgraph.github.io/mxgraph/javascript/examples/editors/images/triangle.gif', 40, 40, 'shape=triangle');
addVertex('https://jgraph.github.io/mxgraph/javascript/examples/editors/images/cylinder.gif', 40, 40, 'shape=cylinder');
addVertex('https://jgraph.github.io/mxgraph/javascript/examples/editors/images/actor.gif', 30, 40, 'shape=actor');
}
}
function addToolbarItem(graph, toolbar, prototype, image)
{
// Function that is executed when the image is dropped on
// the graph. The cell argument points to the cell under
// the mousepointer if there is one.
var funct = function(graph, evt, cell, x, y)
{
graph.stopEditing(false);
var vertex = graph.getModel().cloneCell(prototype);
vertex.geometry.x = x;
vertex.geometry.y = y;
graph.addCell(vertex);
graph.setSelectionCell(vertex);
}
// Creates the image which is used as the drag icon (preview)
var img = toolbar.addMode(null, image, function(evt, cell)
{
var pt = this.graph.getPointForEvent(evt);
funct(graph, evt, cell, pt.x, pt.y);
});
// Disables dragging if element is disabled. This is a workaround
// for wrong event order in IE. Following is a dummy listener that
// is invoked as the last listener in IE.
mxEvent.addListener(img, 'mousedown', function(evt)
{
// do nothing
});
// This listener is always called first before any other listener
// in all browsers.
mxEvent.addListener(img, 'mousedown', function(evt)
{
if (img.enabled == false)
{
mxEvent.consume(evt);
}
});
mxUtils.makeDraggable(img, graph, funct);
return img;
}
</script>
</head>
<!-- Calls the main function after the page has loaded. Container is dynamically created. -->
<body onload="main();" >
</body>
</html>
This uses the XML encoding of mxgraph. You see this code in one of the examples of mxGraph. While it is not json, it is very easy to turn into json, and can be edited or saved as you wish. Now it it stored in localStorage as you asked.
Add the following lines at the end of your main function (after the last call to addVertex):
// read state on load
if(window.localStorage.graphState){
var doc = mxUtils.parseXml(window.localStorage.graphState);
var dec = new mxCodec(doc);
dec.decode(doc.documentElement, graph.getModel());
}
// save state on change
graph.getModel().addListener('change',function(){
var codec = new mxCodec();
window.localStorage.graphState = codec.encode(
graph.getModel()
).outerHTML;
});
Update using JSON
Update 2: Switched to my own custom html2json and json2html implementation to make the answer fully self-contained
If you do need a proper JSON object, you can convert from xml to json and back. Below you can see I stringify and parse the json object, this is only needed to save it in localStorage though.
First add these two functions to convert from html/xml to json and from json back to html/xml:
function html2json(html){
if(html.nodeType==3){
return {
"tagName":"#text",
"content":html.textContent
}
}
var element = {
"tagName":html.tagName
};
if(html.getAttributeNames().length>0){
element.attributes = html.getAttributeNames().reduce(
function(acc,at){acc[at]=html.getAttribute(at); return acc;},
{}
);
}
if(html.childNodes.length>0){
element.children = Array.from(html.childNodes)
.filter(
function(el){
return el.nodeType!=3
||el.textContent.trim().length>0
})
.map(function(el){return html2json(el);});
}
return element;
}
function json2html(json){
var xmlDoc = document.implementation.createDocument(null, json.tagName);
var addAttributes = function(jsonNode, node){
if(jsonNode.attributes){
Object.keys(jsonNode.attributes).map(
function(name){
node.setAttribute(name,jsonNode.attributes[name]);
}
);
}
}
var addChildren = function(jsonNode,node){
if(jsonNode.children){
jsonNode.children.map(
function(jsonChildNode){
json2htmlNode(jsonChildNode,node);
}
);
}
}
var json2htmlNode = function(jsonNode,parent){
if(jsonNode.tagName=="#text"){
return xmlDoc.createTextNode(jsonNode.content);
}
var node = xmlDoc.createElement(jsonNode.tagName);
addAttributes(jsonNode,node);
addChildren(jsonNode,node);
parent.appendChild(node);
}
addAttributes(json,xmlDoc.firstElementChild);
addChildren(json,xmlDoc.firstElementChild);
return xmlDoc;
}
Then change my JavaScript snippet from above like this to transform the xml into json and back:
// read state on load
if(window.localStorage.graphState){
var doc = json2html(JSON.parse(localStorage.graphState));
var dec = new mxCodec(doc);
dec.decode(doc.documentElement, graph.getModel());
}
// save state on change
graph.getModel().addListener('change',function(){
var codec = new mxCodec();
window.localStorage.graphState = JSON.stringify(html2json(codec.encode(
graph.getModel()
)));
});

Printing the map from the canvas is incomplete even after tiles loaded and postrender fired

I am trying to print the map that I create on the fly. However the image that I am getting from the map canvas sometimes looks half baked:
In the screen shot, the top portion is the openlayers map that eventually got rendered and the bottom portion is what I captured from the canvas after postrender.
I am starting from the official Export PDF example, but I am working with a map that just got created. I keep a track of the tiles loading and the tiles loaded through the tileloadstart and tileloadend map events. I also tried incorporating the postrender event as per this answer on stackoverflow. When all the tiles have been loaded I wait for the postrender event to capture the map image from the canvas.
The problem:
I think the problem has to do with the postrender event firing off two early and I don't know how much time to wait after the event before capturing the image from the canvas. I started with a delay of 100 milliseconds and then moved to 1 second and it seemed like it was working fine. But if I add more layers I had to take the delay to 3 seconds for it to start behaving correctly. So I don't know what the delay should be for maps with a lot of layers.
I also get the problem that sometimes the tileloadstart and tileloadend event keeps on firing even after postrender because some tiles had yet to be loaded and even though the tile load count had equalized.
I have created a codepen to demonstrate the issue:
Print map using tile loading and postrender events
var bingKey = ""; // Please enter a bing key
var exportButton = document.getElementById("prepareMap");
exportButton.addEventListener("click", function() {
exportButton.disabled = true;
document.body.style.cursor = "progress";
prepareMap();
});
var map;
var mapCanvas;
var printDelayAfterLoad = 100;
var loading;
var loaded;
var allLoaded;
var prepareMap = function() {
loading = 0;
loaded = 0;
allLoaded = false;
var raster = new ol.layer.Tile({
source: new ol.source.OSM(),
opacity: 0.7
});
var bingLayer = new ol.layer.Tile({
source: new ol.source.BingMaps({
key: bingKey,
imagerySet: "Aerial"
})
});
var mapLayers = [bingLayer, raster];
map = new ol.Map({
layers: mapLayers,
target: "map",
controls: ol.control.defaults({
attributionOptions: {
collapsible: false
}
}),
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
for (var i = 0; i < mapLayers.length; i++) {
var layerSource = mapLayers[i].getSource();
layerSource.on("tileloadstart", tileLoadStart);
layerSource.on("tileloadend", tileLoadEnd);
layerSource.on("tileloaderror", tileLoadEnd);
}
map.renderSync();
};
var tileLoadStart = function() {
++loading;
if (allLoaded) {
logExtraTilesLoadError();
}
};
var tileLoadEnd = function() {
++loaded;
if (loading === loaded) {
allLoaded = true;
listenToPostRender();
}
};
var alreadyPrinted = false;
var listenToPostRender = function() {
map.once("postrender", function() {
if (!alreadyPrinted) {
console.log("postrender called");
window.setTimeout(function() {
printMap();
}, printDelayAfterLoad);
alreadyPrinted = true;
}
});
// listening to postcompose just to capture the mapCanvas from the event
map.once("postcompose", function(event) {
console.log("postcompose called");
mapCanvas = event.context.canvas;
});
};
var printMap = function() {
console.log("printMap called");
var data = mapCanvas.toDataURL("image/png");
var mapImageElement = document.getElementById("mapImage");
mapImageElement.setAttribute("src", data);
cleanUp();
};
var cleanUp = function() {
console.log("cleanUp called");
//exportButton.disabled = false;
document.body.style.cursor = "auto";
};
var errorMessage =
" more tiles loaded after initially all tiles had been loaded";
var extraTileLoadCount = 0;
var logExtraTilesLoadError = function() {
extraTileLoadCount++;
var errorsDiv = document.getElementById("errorsDiv");
errorsDiv.innerHTML = extraTileLoadCount + errorMessage;
console.error("errorMessage");
};
You would need to enter a Bing Maps Key at the top of the javascript file. A Bing Map Key can be generated from: https://www.bingmapsportal.com/. With the BingMap layer the problem is reproducible more consistently. In the code pen I am also capturing the problem when tileloadstart is called after the postrender and writing an error to the top of the page. Also please note that the behavior is inconsistent and you might have to reload several times for it to reproduce.

Paper.js Background Rasterization Glitches

We are building an image editor of sorts using Paper.js. We have a queue on the side of the Paper.js canvas that allows switching between images. Each time we switch between images, we want to flatten all the annotations (rasterize) onto the image just being edited.
Each time we switch images, this method is called, which rasterizes the current image and annotations to a data URL. (If we revisit this image, a raster from this data URL will be displayed.)
var flattenToDataUrl = function() {
layerAsRaster = paper.project.layers[0].rasterize(); // Layer to Paper.js Raster object
layerAsRaster.visible = false; // Attempt to set not visible
var dataString = layerAsRaster.toDataURL();
return dataString;
};
Then we end up calling this method, which changes out the image we're editing:
var setCanvasImage = function(imageObject) {
if(imageObject != null)
{
imageHeight = imageObject.height;
var imageWidth = imageObject.width;
// Set up HTMLImage
var imageElement = new Image(imageObject.width, imageObject.height);
if(_.has(imageObject, 'imageData')) { // Came as 64 bit data
imageElement.src = 'data:image/png;base64,' + imageObject.imageData;
} else if(_.has(imageObject, 'imageUrl')) { // Came as URL
imageElement.src = imageObject.imageUrl;
}
// Add image to Paper.js canvas
imageElement.onload = function(event) {
// Initialize Paper.js on the canvas
paper.setup(canvas);
raster = new paper.Raster(imageElement, new paper.Point(canvas.width / 2, canvas.height / 2));
setUpNotes();
selectedItems = new paper.Group(); // Since Paper.js has been setup we can now initialize this Group
registerCanvasEvents(); // Panning, zooming, moving selected items, deselecting all selected items
fitToPage();
};
}
};
So, this changes out the image, but when I move my mouse into the canvas after selecting a different image in the queue, it glitches to the image we were just on (with its annotations) until I do something like pan, zoom, etc. Then I see the image I selected and am truly working with.
Removing the flattenToDataUrl() functionality makes the queue work seamlessly. So it seems to me something is fishy there. We are generating a Paper.js Raster object in that method. Rasters seem to automatically add themselves. I attempt to curb this with a call to
layerAsRaster.visible = false;
but to no avail.
What is causing this glitchy behavior and how do I prevent it?
Update
For clarity (hopefully) and completeness, I've decided to post the whole PaperFunctions class we use in conjunction with React, which hosts our <canvas> element. There's a lot of code, and a lot of cleanup to do, especially in registerCanvasEvents(). Bear with this learning beginner. Also it's several hundred lines, so it may be helpful to paste it into your favorite editor. Entry points include setCanvas which is called in componentDidMount of the React class with the <canvas> element, and canvasSetImage which is called from the queue. I agree from bmacnaughton's answer that it's weird to call paper.setup(canvas) every time we load a new image. I'm currently investigating the right solution to this, the right place to put it. setCanvas seems logical but when I drag the image to move it in that setup, it leaves a trail of images in its wake. Anyway, here's PaperFunctions.js:
var JQueryMousewheel = require('jquery-mousewheel')($);
var SimplePanAndZoom = require('./SimplePanAndZoom.js');
var panAndZoom = new SimplePanAndZoom();
var selectedItems;
// We use selection here in two distinct ways.
// An item may be Paper.js selected but not in the selection group.
// This is because we want to show it is selectable.
// A blue bounding box indicates it is selectable.
// A green bounding box indicates it has actually been selected and added to selectedItems.
// Only things in selectedItems are actually operated on.
// So the event handlers in this method basically set up whether or not the item is in selectedItems (and therefore will be operated on for moving, resizing, deleting, etc.).
// That is, the event handlers here are concerned with displaying to the user the status of selection for the item - whether or not it will be operated on when events actually happen on the selectedItems Group.
var registerItemEvents = function(item) {
// Boolean flag for mouseup to know if was drag or click
var dragged;
// For tracking if dragging or clicking is happening
item.on('mousedown', function(e) {
dragged = false;
});
// On click toggle selection
item.on('mouseup', function(event) {
event.stopPropagation(); // Only for item applied to
event.preventDefault();
if(!dragged) {
var justAdded = addIfNotInSelectedItems(item);
if(!justAdded) { // Item was in selection group, remove it
item.remove();
paper.project.activeLayer.addChild(item);
this.selectedColor = paper.project.activeLayer.selectedColor;
//item.selected = false;
}
}
});
// Show as selectable even after has been deselected
item.on('mousemove', function(event) {
this.selected = true;
})
// If not selected, on mouse enter show that it is selectable
item.on('mouseenter', function(event) {
if(!this.selected) {
this.selected = true;
}
});
// If not selected, on mouse leave remove indicator that is selectable
item.on('mouseleave', function(event) {
var isInSelectedItems = selectedItems.getItem(item);
if(this.selected && isInSelectedItems == null) {
this.selected = false;
}
});
// On drag, move item
item.on('mousedrag', function(event) {
dragged = true;
// If user starts dragging automatically select the item
addIfNotInSelectedItems(item);
});
}
var addIfNotInSelectedItems = function(item) {
var isInSelectedItems = selectedItems.getItem(item);
if(isInSelectedItems == null) { // Item not currently in selection group, add it
selectedItems.addChild(item);
item.selectedColor = 'green';
item.selected = true;
return true; // Was added, return true
} else {
return false; // Already in, return false
}
}
var registerCanvasEvents = function() {
if(paper.view != null && canvas != null) {
// Zoom on mousewheel
$(canvas).mousewheel(function(event) {
event.preventDefault();
var mousePosition = new paper.Point(event.offsetX, event.offsetY);
var viewPosition = paper.view.viewToProject(mousePosition);
var returnedValues = panAndZoom.changeZoom(paper.view.zoom, (event.deltaY * -1), paper.view.center, viewPosition, 1.1);
var newZoom = returnedValues[0];
var offset = returnedValues[1];
paper.view.zoom = newZoom;
paper.view.center = paper.view.center.add(offset);
});
// For tracking if dragging or clicking is happening
var dragged;
paper.project.layers[0].on('mousedown', function(e) { // TODO should be layer 0 in long run?
dragged = false;
});
// Pan on mouse drag
/*paper.project.layers[0].on('mousedrag', function(event) { // TODO should be layer 0 in long run?
if(!event.event.ctrlKey && !event.event.altKey && !event.event.shiftKey) { // No keys (that we use) can be pushed
dragged = true; // We're panning, we don't wish to deselect all items as we would do with a click
paper.view.center = panAndZoom.changeCenter(paper.view.center, event.delta.x, event.delta.y, 0.7);
//event.preventDefault();
}
});*/
// Move selected items on mouse drag
selectedItems.on('mousedrag', function(event) {
event.stopPropagation(); // Don't propogate up or it will register as a pan event
event.preventDefault();
dragged = true; // We're panning, we don't wish to deselect all items as we would do with a click
this.translate(new paper.Point(event.delta.x, event.delta.y));
});
// If was a click and not a drag, deselect selected items
paper.project.layers[0].on('mouseup', function(event) {
if(!dragged) {
var removedItems = selectedItems.removeChildren(); // Remove from selection group, which also removes from display
paper.project.activeLayer.addChildren(removedItems); // Return to display
// Reset selection colors for showing selectable
for(var i =0; i < removedItems.length; i++) {
removedItems[i].selectedColor = paper.project.activeLayer.selectedColor;
removedItems[i].selected = false;
}
}
});
// Initial path object, will be reset for new paths after Alt is released
var path = newPath();
var paths = [];
paths.push(path);
// On mousedown add point to start from
paper.project.layers[0].on('mousedown', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(paths[paths.length-1].lastSegment == null) {
//path.add(event.point, event.point);
paths[paths.length-1].add(event.point, event.point);
} else {
//path.add(path.lastSegment.point, path.lastSegment.point);
paths[paths.length-1].add(paths[paths.length-1].lastSegment.point, paths[paths.length-1].lastSegment.point);
}
}
});
// On mousedrag add points to path
paper.project.layers[0].on('mousedrag', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(event.event.shiftKey) { // Use shift key for freeform
//path.add(event.point);
paths[paths.length-1].add(event.point);
} else { // Default of straight line added to path
//path.lastSegment.point = event.point;
paths[paths.length-1].lastSegment.point = event.point;
}
}
}.bind(this));
var tool = new paper.Tool();
var startDragPoint;
// Capture start of drag selection
paper.tool.onMouseDown = function(event) {
if((event.event.ctrlKey && event.event.shiftKey) || (event.event.ctrlKey && event.event.altKey)) {
startDragPoint = new paper.Point(event.point);
}
};
paper.tool.onMouseDrag = function(event) {
// Panning
if(!event.event.ctrlKey && !event.event.altKey && !event.event.shiftKey) { // No keys (that we use) can be pushed
dragged = true; // We're panning, we don't wish to deselect all items as we would do with a click
paper.view.center = panAndZoom.changeCenter(paper.view.center, event.delta.x, event.delta.y, 0.7);
//event.preventDefault();
}
// Show box indicating the area that has been selected
// For moving area and whiting out area
if((event.event.ctrlKey && event.event.shiftKey) || (event.event.ctrlKey && event.event.altKey)) {
dragged = true;
var showSelection = new paper.Path.Rectangle({
from: startDragPoint,
to: event.point,
strokeColor: 'red',
strokeWidth: 1
});
// Stop showing the selected area on drag (new one is created) and up because we're done
showSelection.removeOn({
drag: true,
up: true
});
}
};
// Capture start of drag selection
paper.tool.onMouseUp = function(event) {
if((event.event.ctrlKey && event.event.shiftKey) || (event.event.ctrlKey && event.event.altKey)) {
var endDragPoint = new paper.Point(event.point);
if(event.event.ctrlKey && event.event.shiftKey) { // Whiteout area
whiteoutArea(startDragPoint, endDragPoint);
} else if(event.event.ctrlKey && event.event.altKey) { // Move selected area
selectArea(startDragPoint, endDragPoint);
}
}
};
// Key events
paper.tool.onKeyUp = function(event) {
// Delete selected items on delete key
if(event.key == 'delete') {
selectedItems.removeChildren();
} else if (event.key == 'option') {
registerItemEvents(paths[paths.length-1]);
// Start a new path
paths.push(newPath());
}
}
}
}
// These variables are scoped so that all methods in PaperFunctions can access them
var canvas; // Set by setCanvas
var imageHeight; // Set by setCanvasImage
var raster;
var toolsSetup = false;
var setCanvas = function(canvasElement) {
canvas = canvasElement;
paper.setup(canvas);
};
var setCanvasImage = function(imageObject) {
if(imageObject != null)
{
imageHeight = imageObject.height;
var imageWidth = imageObject.width;
// Set up HTMLImage
var imageElement = new Image(imageObject.width, imageObject.height);
if(_.has(imageObject, 'imageData')) { // Came as 64 bit data
imageElement.src = 'data:image/png;base64,' + imageObject.imageData;
} else if(_.has(imageObject, 'imageUrl')) { // Came as URL
imageElement.src = imageObject.imageUrl;
}
// Add image to Paper.js canvas
imageElement.onload = function(event) {
//canvas.height = $(document).height()-3; // Set canvas height. Why do this here and not in the React component? Because we set the width here too, so we're keeping those together. Perhaps in the future this will be changed when we are responsive to window resizing.
//scalingFactor = canvas.height / imageObject.height; // Determine the ratio
//canvas.width = imageElement.width * scalingFactor; // Scale width based on height; canvas height has been set to the height of the document
// Initialize Paper.js on the canvas
paper.setup(canvas);
raster = new paper.Raster(imageElement, new paper.Point(canvas.width / 2, canvas.height / 2));
//setUpLineAndFreeFormDrawing(); // TODO once we cycle through images will we need to reset this for each new image or can we do this just once?
setUpNotes(); // TODO once we cycle through images will we need to reset this for each new image or can we do this just once?
selectedItems = new paper.Group(); // Since Paper.js has been setup we can now initialize this Group
registerCanvasEvents(); // Panning, zooming, moving selected items, deselecting all selected items
fitToPage();
};
}
};
var fitToPage = function() {
if(paper.view != null && canvas != null) {
// Fit image to page so whole thing is displayed
var scalingFactor = canvas.height / imageHeight; // Constant representation of the ratio of the canvas size to the image size
var zoomFactor = scalingFactor / paper.view.zoom; // Dynamic representation of the zoom needed to return to viewing the whole image in the canvas
// Reset the center point to the center of the canvas
var canvasCenter = new paper.Point(canvas.width/2, canvas.height/2);
paper.view.center = canvasCenter;
// Zoom to fit the whole image in the canvas
var returnedValues = panAndZoom.changeZoom(paper.view.zoom, -1, canvasCenter, canvasCenter, zoomFactor); // Always pass -1 as the delta, not entirely sure why
var newZoom = returnedValues[0];
var offset = returnedValues[1];
paper.view.zoom = newZoom;
paper.view.center = paper.view.center.add(offset);
}
};
var addImage = function(imageDataUrl) {
if(paper.view != null) {
var img = document.createElement("img");
img.src = imageDataUrl;
var presentMomentForId = new Date().getTime() + "-image"; // For purposes of having unique IDs
img.id = presentMomentForId;
img.hidden = true;
document.body.appendChild(img);
var raster = new paper.Raster(presentMomentForId);
registerItemEvents(raster);
}
};
var setUpLineAndFreeFormDrawing = function() {
if(paper.project != null) {
// Initial path object, will be reset for new paths after Alt is released
var path = newPath();
var paths = [];
paths.push(path);
// On mousedown add point to start from
paper.project.layers[0].on('mousedown', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(paths[paths.length-1].lastSegment == null) {
//path.add(event.point, event.point);
paths[paths.length-1].add(event.point, event.point);
} else {
//path.add(path.lastSegment.point, path.lastSegment.point);
paths[paths.length-1].add(paths[paths.length-1].lastSegment.point, paths[paths.length-1].lastSegment.point);
}
}
});
// On mousedrag add points to path
paper.project.layers[0].on('mousedrag', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(event.event.shiftKey) { // Use shift key for freeform
//path.add(event.point);
paths[paths.length-1].add(event.point);
} else { // Default of straight line added to path
//path.lastSegment.point = event.point;
paths[paths.length-1].lastSegment.point = event.point;
}
}
}.bind(this));
// Each time Alt comes up, start a new path
paper.tool.onKeyUp = function(event) {
if(event.key == "option") {
registerItemEvents(paths[paths.length-1]);
// Start a new path
paths.push(newPath());
}
};
}
};
// Establishes default line style
var newPath = function() {
var path = new paper.Path();
path.strokeColor = 'black';
path.strokeWidth = 10;
return path;
};
var note = "";
var setNote = function(newNote) {
note = newNote;
};
var setUpNotes = function() {
if(paper.project != null) {
paper.project.layers[0].on('mousedown', function(event) { // TODO should be layer 0 in long run?
if(event.event.ctrlKey && !event.event.altKey && !event.event.shiftKey) { // Only Ctrl key to add text
// Add text box
var textBox = new paper.PointText(event.point);
textBox.justification = 'left';
textBox.fillColor = 'black';
textBox.fontSize = 60;
textBox.content = note;
registerItemEvents(textBox);
}
});
}
};
var selectArea = function(startDragPoint, endDragPoint) {
var rasterTopLeftCorner = new paper.Point(raster.bounds.topLeft);
var adjustedStartDragPoint = new paper.Point(startDragPoint.x - rasterTopLeftCorner.x, startDragPoint.y - rasterTopLeftCorner.y);
var adjustedEndDragPoint = new paper.Point(endDragPoint.x - rasterTopLeftCorner.x, endDragPoint.y - rasterTopLeftCorner.y);
var boundingRectangleRasterCoordinates = new paper.Rectangle(adjustedStartDragPoint, adjustedEndDragPoint);
var boundingRectangleCanvasCoordinates = new paper.Rectangle(startDragPoint, endDragPoint);
var selectedArea = raster.getSubRaster(boundingRectangleRasterCoordinates);
var whitedOutSelection = new paper.Shape.Rectangle(boundingRectangleCanvasCoordinates);
whitedOutSelection.fillColor = 'white';
whitedOutSelection.insertAbove(raster); // Whiteout just above the image we're working with
registerItemEvents(selectedArea);
}
var whiteoutArea = function(startDragPoint, endDragPoint) {
var whitedOutSelection = new paper.Shape.Rectangle(startDragPoint, endDragPoint);
whitedOutSelection.fillColor = 'white';
whitedOutSelection.insertAbove(raster); // Whiteout just above the image we're working with
}
var flattenToDataUrl = function() {
layerAsRaster = paper.project.layers[0].rasterize(); // TODO should be layer 0 in long run? // Layer to Paper.js Raster object
layerAsRaster.visible = false;
var dataString = layerAsRaster.toDataURL();
return dataString;
};
module.exports = {
setCanvas: setCanvas,
setCanvasImage: setCanvasImage,
fitToPage: fitToPage,
addImage: addImage,
setNote: setNote,
flattenToDataUrl: flattenToDataUrl
};
Additionally, here's the SimplePanAndZoom.js file for clarity. It uses minimal Paper functions, it mainly just does calculations:
// Based on http://matthiasberth.com/articles/stable-zoom-and-pan-in-paperjs/
var SimplePanAndZoom = (function() {
function SimplePanAndZoom() { }
SimplePanAndZoom.prototype.changeZoom = function(oldZoom, delta, centerPoint, offsetPoint, zoomFactor) {
var newZoom = oldZoom;
if (delta < 0) {
newZoom = oldZoom * zoomFactor;
}
if (delta > 0) {
newZoom = oldZoom / zoomFactor;
}
// Zoom towards offsetPoint, not centerPoint (unless they're the same)
var a = null;
if(!centerPoint.equals(offsetPoint)) {
var scalingFactor = oldZoom / newZoom;
var difference = offsetPoint.subtract(centerPoint);
a = offsetPoint.subtract(difference.multiply(scalingFactor)).subtract(centerPoint);
}
return [newZoom, a];
};
SimplePanAndZoom.prototype.changeCenter = function(oldCenter, deltaX, deltaY, factor) {
var offset;
offset = new paper.Point(-deltaX, -deltaY);
offset = offset.multiply(factor);
return oldCenter.add(offset);
};
return SimplePanAndZoom;
})();
module.exports = SimplePanAndZoom;
Thanks.
I'm taking some guesses here but I'll address some problems in the code that will hopefully address the behavior you're seeing.
First, I presume paper.project.layers[0] is paper.project.activeLayer. Once that has been rasterized 1) the raster is added to the layer and setting visible = false does cause it to disappear when an update is done.
Second, when you invoke paper.setup(canvas) in imageElement.onload you create a new paper project. This project starts out as the active project and makes the previous project "disappear". So when you create a raster with raster = new paper.Raster(...) it goes into the new project, not the old project.
So now there is a hidden (.visible = false) raster in the old project (let's call it project1) and a new version of it in project2.
I'm not sure if this is the intended behavior or not, but when you invoke paper.setup(canvas) for what seems to be the second time then paper seems to notice that they both refer to the same canvas and keeps project1 and project2 in sync. So creating the second project clears the first project's children array. And adding new paper.Raster(...) ends up adding the raster to project1 and project2.
Now I can't tell what the next piece of the puzzle is. You'd need to add some information like 1) where the mouse event handlers are setup and what they are attached to, 2) what setUpNotes() does, 3) what registerCanvasEvents() does, and 4) what fitToPage does.
There are a few globals created, imageHeight and raster that probably aren't intentional. And it's not clear why you need to use new Image() at all - paper.Raster() accepts URLs, including data URLs.
I was surprised paper cleared the first project. It's curious.
Version 2:
Let me take a stab at structuring this using layers. I'd suggest you get rid of multiple projects because having mouse event handlers attached to multiple projects that share the same canvas adds too much complexity.
So, in your code initialization: paper.setup(canvas). Do this once and only once.
Setup the initial image in the single layer initially created by paper.
// this will be inserted into the current layer, project.activeLayer
var raster = new paper.Raster(imageURL, paper.view.bounds.center);
When the image in your queue changes do something like:
// make the existing image/layer invisible
paper.project.activeLayer.visible = false;
// add a new layer which is inserted in the project and activated
var layer = new paper.Layer();
// the new layer is activated, create a raster for the image
var raster = new paper.Raster(imageURL, paper.view.bounds.center);
// now do your normal logic for editing, zooming, etc.
It's really a bit more complicated than that because you have a queue of images and you only want to create a layer the first time you visit an image. You could initialize all the rasters at the outset, something like:
var imageURLs = ["url to image1", "url to image2", "etc"];
imageURLs.forEach(function(url) {
new paper.Layer();
paper.project.activeLayer.visible = false;
new paper.Raster(url, paper.view.bounds.center);
});
// make the first layer visible and activate it
paper.project.layers[0].visible = true;
paper.project.layers[0].activate();
The preceeding code gives you a parallel array to the images in your queue so switching images is straightforward - there is no checking to see if that image has been created or not:
function setImage(index) {
paper.project.activeLayer.visible = false;
paper.project.layers[index].activate();
paper.project.layers[index].visible = true;
}
Finally, I would make sure my mouse handling wasn't causing me problems. From the new code you posted it looks like each project had a global tool that handled 'mousedown', 'mousedrag', and 'mouseup' events, another set of handlers for activeLayer for 'mousedown', 'mousedrag', and 'mouseup' events, and also selectedItems has a handler for 'mousedrag'. I can't keep track of what all the different handlers are supposed to do across projects. I'm guessing that these are the root issue with the flickering you saw.
I would likely just use paper.view.on for 'mousedown', 'mousedrag', and 'mouseup' events. When I get an event I would check to see if anything on the layer was hit by using the following:
paper.project.activeLayer.hitTest(event.point);
Being able to set events on the view is new for paper but very useful. There may be a few other tweaks necessary to handle highlighting unselected items. A relatively straightforward way to handle that is to have a group of selected items and a group of unselected items:
unSelectedGroup.on('mouseenter', function() {
unSelectedGroup.selected = true;
});
unSelectedGroup.on('mouseleave', function() {
unSelectedGroup.selected = false;
});
These should be safe across layers when only one layer is visible at a time. I would set up these group handlers when setting up the images, whether all up front or on an as-needed basis. Alternatively, you could also add paper.view.on('mousemove', ...) and handle the 'mouseenter' and 'mouseleave' events yourself using hitTest as shown above, but either approach should work.
I think using a layer-based approach to your images will keep things in sync. There are enough problems with the project-based approach and many different mouse event handlers that you'll be on more stable ground regardless.

Getting click on Mapbox markers

I have been able to integrate markers to the mapbox we are using, but still wonder if we can get a click on them. If so how?
Following is my code:
<style>
/*
* Unlike other icons, you can style `L.divIcon` with CSS.
* These styles make each marker a circle with a border and centered text.
*/
.count-icon1 {
background:url(images/redpin.png);
color:#000;
font-weight:600;
text-align:center;
padding:19px 0 0 0px; font-size:180%;
}
.count-icon2 {
background:url(images/greenpin.png);
color:#000;
font-weight:600;
text-align:center;
padding:19px 0 0 0px; font-size:180%;
}
</style>
js code:
var defaultLat = 39.12367;
var defaultLon = -76.81229;
if($scope.currentLocDetails != null){
if($scope.currentLocDetails.Lat != null && $scope.currentLocDetails.Lon != null){
defaultLat = $scope.currentLocDetails.Lat;
defaultLon = $scope.currentLocDetails.Lon;
}
}
var x = 0;
if(map != null)
map.remove();
map = L.mapbox.map('map_view', 'your key here').setView([defaultLat, defaultLon], 9);
for (var i = 0; i < responseData.JobLocation.length; i++) {
var eachObj = responseData.JobLocation[i];
if(eachObj.Lat != null && eachObj.Lon != null){
x++;
// Use a little math to position markers.
// Replace this with your own code.
L.marker([
eachObj.Lat,
eachObj.Lon
], {
icon: L.divIcon({
// Specify a class name we can refer to in CSS.
className: ((currentSelectedIndex + 1) == i + 1)?'count-icon1':'count-icon2',
// Define what HTML goes in each marker.
html: i + 1,
// Set a markers width and height.
iconSize: [65, 94]
})
}).addTo(map);
}
}
I tried doing a bit R & D, but get to no where:
We need to use featureLayer, but dunno how.
For the click feature we need to follow this code, but how?
// Listen for individual marker clicks.
myLayer.on('click',function(e) {
// Force the popup closed.
e.layer.closePopup();
var feature = e.layer.feature;
var content = '<div><strong>' + feature.properties.title + '</strong>' +
'<p>' + feature.properties.description + '</p></div>';
info.innerHTML = content;
});
Any help with this is really appreciated.
Thanks
I believe there are various ways of doing this with Mapbox, unfortunately I don't have access to my project where I use it right now so I'm just going off the Mapbox documentation.
Following your example this looks the simplest - if you have added marker, for example:
var marker = L.marker([43.6475, -79.3838], {
icon: L.mapbox.marker.icon({
'marker-color': '#9c89cc'
})
})
.bindPopup('<p>Your html code here</p>')
.addTo(map);
You can pass whatever HTML you want in the bindPopup argument.
https://www.mapbox.com/mapbox.js/example/v1.0.0/clicks-in-popups/
Then it should be be pretty simple via 'addEventListener('click', function)' on that marker variable.
Or alternatively -
myLayer.on('click', function(e) {
resetColors();
e.layer.feature.properties['old-color'] = e.layer.feature.properties['marker-color'];
e.layer.feature.properties['marker-color'] = '#ff8888';
myLayer.setGeoJSON(geoJson);
});
map.on('click', resetColors);
Effectively add an event listener on the map variable - and then listen to what you've clicked on via the event argument passed to the event listener.
This may be useful: https://www.mapbox.com/mapbox.js/example/v1.0.0/change-marker-color-click/
Good luck!

Categories

Resources