How to remove specific group of markers? Google Maps API - javascript

I want to make few groups and when I toggle specific I want to make it hidden. I will show you what I have made so far. But this hides everything, and I know why, because this marker.setVisible(true); the "marker" is global and it is not for specific group, but I don't know how to make it for specific group.
EDIT: Might not work, idk how to include js file...
Javascript
// SanMap.js
// Tool for drawing Google Maps of San Andreas.
// Written by Tim Potze
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// For more information, please refer to <http://unlicense.org>
//
/* Create a set of helper classes.
*/
/**
* Projection specialized for San Andreas, based on GallPetersProjection
* available at:
* https://developers.google.com/maps/documentation/javascript/examples/map-projection-simple
* #class SanMapProjection
* #constructor
* *implements {google.maps.Projection}
*/
function SanMapProjection(tileSize) {
/**
* The range across the map.
*
* #property projectionRange_
* #type {Number}
*/
var projectionRange_ = tileSize;
/**
* The origin of the map.
*
* #property pixelOrigin_
* #type {Object}
*/
this.pixelOrigin_ = new google.maps.Point(projectionRange_ / 2,
projectionRange_ / 2);
/**
* The number of pixels per longitude degree.
*
* #property pixelsPerLonDegree_
* #type {Number}
*/
this.pixelsPerLonDegree_ = projectionRange_ / 360;
/**
* Converts a google.maps.LatLng to google.maps.Point.
*
* #method fromLatLngToPoint
* #param {Object} latLng The LatLng object to convert.
* #param {Object} opt_point optional point type to use as return type
* instead of google.maps.Point.
* #return {Object} The newly created point.
*/
this.fromLatLngToPoint = function (latLng, opt_point) {
var point = opt_point || new google.maps.Point(0, 0);
point.x = this.pixelOrigin_.x + latLng.lng() *
this.pixelsPerLonDegree_ * 2;
point.y = this.pixelOrigin_.y - latLng.lat() *
this.pixelsPerLonDegree_ * 2;
return point;
}
/**
* Converts a google.maps.Point to google.maps.LatLng.
*
* #method fromLatLngToPoint
* #param {Object} point The Point object to convert.
* #return {Object} The newly created LatLng.
*/
this.fromPointToLatLng = function (point) {
var lng = (point.x - this.pixelOrigin_.x) /
this.pixelsPerLonDegree_ / 2;
var lat = (-point.y + this.pixelOrigin_.y) /
this.pixelsPerLonDegree_ / 2;
return new google.maps.LatLng(lat, lng, true);
}
};
/**
* Simple class for providing a google.maps.ImageMapType based on the provided
* zoom limitations and function for providing tiles.
* #class SanMapType
* #constructor
*/
function SanMapType(minZoom, maxZoom, getTileUrl, tileSize) {
/**
* Creates an instance of google.maps.ImageMapType based on the provided
* zoom limitations and function for providing tiles.
*
* #method getImageMapType
* #param {Boolean} repeating Whether the map should repeat horizontally.
* #return {Object} The newly created ImageMapType.
*/
this.getImageMapType = function (repeating) {
/* Default tileSize to 512.
*/
tileSize = tileSize || 512;
return new google.maps.ImageMapType({
getTileUrl: function (coord, zoom) {
var x = coord.x,
y = coord.y,
max = 1 << zoom;
/* If not repeating and x is outside of the range -or- y is
* outside of the range, return a clear tile. This can be
* provided by getTileUrl, using the tile coordinates (-1, -1).
*/
if (y < 0 || y >= max ||
(repeating !== true && (x < 0 || x >= max))) {
return getTileUrl(zoom, -1, -1);
}
/*
* Return the provided tile. Make sure x is within the
* range 0 - max.
*/
return getTileUrl(zoom, (((x % max) + max) % max), y);
},
tileSize: new google.maps.Size(tileSize, tileSize),
maxZoom: maxZoom,
minZoom: minZoom
});
}
};
/* Define a number of SanMap methods.
*/
function SanMap(){ }
/**
* Creates an instance of google.maps.Map with the provided map types.
*
* #method createMap
* #param {Object} canvas The element to draw the map on.
* #param {Number} mapTypes The map types available in the map control.
* #param {Number} zoom The initial zoom level.
* #param {Object} center The intial center point to focus on.
* #param {Boolean} repeating Whether the map should repeat horizontally.
* #param {String} defaultMap The default map type.
* #return {Object} The newly created Map.
*/
SanMap.createMap = function(canvas, mapTypes, zoom, center, repeating,
defaultMap) {
/* If no mapTypes are parsed, return null and display a warning
*/
if (mapTypes === undefined || mapTypes.length == 0) {
console.warn("SanMap: No map types were parsed with SanMap.createMap.");
return null;
}
/* Create the map
*/
var map = new google.maps.Map(canvas, {
zoom: zoom || 2,
center: center || SanMap.getLatLngFromPos(90, -90),
zoomControl: true,
zoomControlOptions: {
position: google.maps.ControlPosition.RIGHT_TOP,
},
streetViewControl: false,
//mapTypeControl: false,
options: {
gestureHandling: 'greedy'
},
mapTypeControlOptions: {
mapTypeIds: Object.keys(mapTypes)
},
//restriction: {latLngBounds:{north: 90, south: -90, west: -90, east: 90}, strictBounds: false,}
});
/* Add every map type to the map.
*/
for (var key in mapTypes) {
if (mapTypes.hasOwnProperty(key)) {
var type = mapTypes[key].getImageMapType(repeating || false);
type.name = type.alt = key;
type.projection = new SanMapProjection(type.tileSize.width);
map.mapTypes.set(key, type);
}
}
/* Set the default map type.
*/
map.setMapTypeId(defaultMap || Object.keys(mapTypes)[0]);
/* If not repeating, bound the viewable area.
*/
if (!repeating) {
bounds = new google.maps.LatLngBounds(new google.maps.LatLng(-90,-90),
new google.maps.LatLng(90,90));
/* When the center changes, check if the new center is within the bounds
* of the map. If not, move the center to within these bounds.
*/
google.maps.event.addListener(map, 'center_changed', function () {
if (bounds.contains(map.getCenter()))
return;
var lng = map.getCenter().lng(),
lat = map.getCenter().lat();
if (lng < bounds.getSouthWest().lng())
lng = bounds.getSouthWest().lng();
if (lng > bounds.getNorthEast().lng())
lng = bounds.getNorthEast().lng();
if (lat < bounds.getSouthWest().lat())
lat = bounds.getSouthWest().lat();
if (lat > bounds.getNorthEast().lat())
lat = bounds.getNorthEast().lat();
map.setCenter(new google.maps.LatLng(lat, lng));
});
}
return map;
};
/* Conversion properties. */
SanMap.width = 6000;
SanMap.height = 6000;
SanMap.ox = 0;
SanMap.oy = 0;
/**
* Set the properties of the map coordinate system.
*
* #method setMapSize
* #param {Number} width The width of the map.
* #param {Number} y The GTA:SA y-coordinate.
*/
SanMap.setMapSize = function (width, height, offsetx, offsety) {
SanMap.width = width;
SanMap.height = height;
SanMap.ox = offsetx;
SanMap.oy = offsety;
}
/**
* Converts a GTA:SA coordinates to an instance of google.maps.LatLng.
*
* #method getLatLngFromPos
* #param {Number} x The GTA:SA x-coordinate.
* #param {Number} y The GTA:SA y-coordinate.
* #return {Object} The newly created LatLng.
*/
SanMap.getLatLngFromPos = function (x, y) {
return typeof(x) == "object"
? new google.maps.LatLng((x.y - SanMap.oy) / SanMap.height * 180, (x.x - SanMap.ox) / SanMap.width * 180)
: new google.maps.LatLng((y - SanMap.oy) / SanMap.height * 180, (x - SanMap.ox) / SanMap.width * 180);
}
/**
* Converts an instance of google.maps.LatLng to a GTA:SA coordinates.
*
* #method getPosFromLatLng
* #param {Object} latLng The LatLng to convert..
* #return {Object} An Object containing the GTA:SA coordinates.
*/
SanMap.getPosFromLatLng = function (latLng) {
return {x: latLng.lng() * SanMap.width / 180 + SanMap.ox, y: latLng.lat() * SanMap.height / 180 + SanMap.oy};
}
html, body, #map-canvas {
height: 100%;
padding: 0;
margin: 0;
}
<html>
<head>
<title>Old School - Mapa</title>
<!-- Disallow users to scale this page -->
<meta name="viewport" content="initial-scale=1.0, user-scalable=no"/>
<link rel="stylesheet" href="style.css">
</head>
<body>
<input id="removeMarker" type="checkbox" checked="checked"></input> <!-- Toggle marker group 1 -->
<input id="removeMarker2" type="checkbox" checked="checked"></input> <!-- Toggle marker group 2 -->
<!-- The container the map is rendered in -->
<div id="map-canvas"></div>
<!-- Load all javascript -->
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="https://maps.google.com/maps/api/js?sensor=false&key=YOUR_API_KEY"></script>
<script src="SanMap.js"></script>
<script>
var mapType = new SanMapType(1, 3, function (zoom, x, y) {
return x == -1 && y == -1 ? "tiles/map.outer.png" : "tiles/map." + zoom + "." + x + "." + y + ".png";
});
var satType = new SanMapType(1, 3, function (zoom, x, y) {
return x == -1 && y == -1 ? "tiles/map.outer.png" : "tiles/sat." + zoom + "." + x + "." + y + ".png";
});
var map = SanMap.createMap(document.getElementById('map-canvas'),
{'Original': mapType, 'Satelit': satType}, 1, null, false, 'Original');
var locations = [ // Marker group 1
["Burg", 1215.7954,-923.9620, 'images/icon96.png']
];
var locations2 = [ // Marker group 2
["Casino", 2043.4570,1918.1044, 'images/icon89.png']
];
for (var i = 0; i < locations.length; i++) { // Marker group 1
placeMarker(locations[i]);
}
for (var i = 0; i < locations2.length; i++) { // Marker group 2
placeMarker(locations2[i]);
}
function placeMarker(loc) { // Get markers from all groups
var latLng = SanMap.getLatLngFromPos(loc[1], loc[2]);
var marker = new google.maps.Marker({
position: latLng,
map: map,
optimized: true,
icon: loc[3]
});
$('#removeMarker').click(function(event) { // Remove group 1 marker
if(this.checked)
{
$(':checkbox').each(function()
{
this.checked = true;
});
for (var i = 0; i < locations.length; i++) { marker.setVisible(true); }
}
else
{
$(':checkbox').each(function()
{
this.checked = false;
});
for(var i = 0; i < locations.length; i++) { marker.setVisible(false); }
}
});
$('#removeMarker2').click(function(event) { // Remove group 2 marker
if(this.checked)
{
$(':checkbox').each(function()
{
this.checked = true;
});
for (var i = 0; i < locations2.length; i++) { marker.setVisible(true); } // Here must be done something to be able to remove specific marker for that group
}
else
{
$(':checkbox').each(function()
{
this.checked = false;
});
for(var i = 0; i < locations2.length; i++) { marker.setVisible(false); } // Here must be done something to be able to remove specific marker for that group
}
});
var infowindow = new google.maps.InfoWindow();
google.maps.event.addListener(marker, 'click', function() {
infowindow.setContent("<div id='infowindow'>" + loc[0] + "</div>");
infowindow.open(map, marker);
});
map.addListener('click', function() { if(infowindow) infowindow.close(); });
}
</script>
</body>

The problem is that you call setVisible() method on the same marker each time when you loop in the locations array. to achieve what you want you need to store the markers objects when you create them, and after, when you need it, access it to delete them.
1 - Store markers object in array
First, create arrays corresponding to each group you want to create. After that, when you create markers, store them in the array corresponding of his category.
// Array for saving markers
let loc1Markers = []; // markers object from group 1
let loc2Markers = []; // markers object from group 2
// loop for creation of markers in each group
for (let i=0; i<locations.length; i++){
loc1Markers.push(placeMarker(locations[i])); // Marker pushed in group 1
}
for (var i = 0; i < locations2.length; i++) {
loc2Markers.push(placeMarker(locations2[i])); // Marker pushed in group 2
}
function placeMarker(loc){
// ... here the content of your function
return marker // return the marker object that you just created
}
In this exemple, the object marker created is returned at the end of the placeMarker function, and it is store directly in his corresponding array.
Loop through the marker array
Then, you can access each of your desired group by looping in your arrays of markers object. Modify all the lines like
for (var i = 0; i < locations.length; i++) { marker.setVisible(true); }
by calling directly the array of the group you want. Something like this for exemple :
for(let i=0; i<loc1Markers.length; i++){
loc1Markers[i].setVisible(true);
}
In this case you loop in the array with all of the markers of the group you want and when you call the setVisible() method, it's on different marker each time.

Related

Update Geojson file using Geolocation [Onclick]

I have a local .json file with lat/long coordinates for each feature, and a column Distance that is empty (randomly putting 999).
The overall goal is to display markers in a leaflet map and filter it based on geolcoation using a button.
I am trying to create one button that is able to:
Get my current location
Calculate distance for each feature in my json file (for loop)
[Blocking point] Update the column Distance / or create a new column
filter json file based on distance (for ex. show me markers where distance is under 100 km)
I got inspiration from this example
but had suffered with the "callback hell" issue.
I am right now in the 3rd step : I managed to calculate distance. but not working outside the function.
Here the code I'm using, The blockage is in the For loop: the distance column is not being updated
var allmarkers = L.markerClusterGroup();
var markersdist100 = L.markerClusterGroup();
// Load json file
var promise = $.getJSON("./data/FILE.geojson");
promise.then(function(data) {
// Update distance in json data
// Geolocation part to get current position
var userPositionPromise = new Promise(function(resolve, reject) {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(data1) {
resolve(data1);
}, function(error) {
reject(error);
});
} else {
reject({
error: 'browser doesn\'t support geolocation'
});
};
});
userPositionPromise.then(function(dataa) {
lat = dataa.coords.latitude;
lng = dataa.coords.longitude;
console.log(lng); // check ok : lng of current location
console.log(data.features.length); //check ok : json length for iteration
// For loop to calculate the new distance
for (var i = 0; i < data.features.length; i++) {
data.features[i].properties.distance = getDistanceFromLatLonInKm(lat, lng, data.features[i].geometry.coordinates[0], data.features[i].geometry.coordinates[1]);
console.log(data.features[i].properties.distance); //check ok : showing the right distance
}
})
console.log(data.features[0].properties.distance); //check BUG : showing initial distance [999]
//all data
var all = L.geoJson(data, {
pointToLayer: style_markers,
});
// data filtered by distance, see the function [filter_distance]
var distance100 = L.geoJson(data, {
pointToLayer: style_markers,
filter: filter_distance
});
// Filter distance on button click
$("#distance100").click(function() {
markersdist100.addLayer(distance100);
allmarkers.addLayer(all);
map.addLayer(markersdist100);
map.removeLayer(allmarkers);
});
});
// FUNCTIONS
function filter_distance(feature, other) {
return feature.properties.distance < 100;
};
function getDistanceFromLatLonInKm(lat1, lon1, lat2, lon2) {
var R = 6371; // Radius of the earth in km
var dLat = deg2rad(lat2 - lat1); // deg2rad below
var dLon = deg2rad(lon2 - lon1);
var a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c; // Distance in km
return d;
};
function deg2rad(deg) {
return deg * (Math.PI / 180)
};
Do you have any recommendations on how to structure my code to access the data with the updated Distance column
Thank you
I got a solution !
I had only to put the var distance100 right after the For loop
With these functions : On button click, you are able to filter markers in a Leaflet map based on your current location (by updating distance in your json file)
Many cases are tackled here : read/access and update a Geojson local file, filter Leaflet markers onclick, use geolocation coordinates in another function, calculate distance ...
the updated code is below (loading the map and the controls is not covered):
var allmarkers = L.markerClusterGroup(); //markercluster plugin using leaflet
var markersdist100 = L.markerClusterGroup();
// Load json file
var promise = $.getJSON("./data/FILE.geojson");
promise.then(function(data) {
// Update distance in json data
// Geolocation part to get current position
var userPositionPromise = new Promise(function(resolve, reject) {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(data1) {
resolve(data1);
}, function(error) {
reject(error);
});
} else {
reject({
error: 'browser doesn\'t support geolocation'
});
};
});
userPositionPromise.then(function(dataa) {
lat = dataa.coords.latitude;
lng = dataa.coords.longitude;
console.log(lng); // check ok : lng of current location
console.log(data.features.length); //check ok : json length for iteration
// For loop to calculate the new distance
for (var i = 0; i < data.features.length; i++) {
data.features[i].properties.distance = getDistanceFromLatLonInKm(lat, lng, data.features[i].geometry.coordinates[0], data.features[i].geometry.coordinates[1]);
console.log(data.features[i].properties.distance); //check ok : showing the right distance
};
// data filtered by distance, see the function [filter_distance]
var distance100 = L.geoJson(data, {
pointToLayer: style_markers,
filter: filter_distance
});
// Filter distance on button click
$("#distance100").click(function() {
markersdist100.addLayer(distance100);
allmarkers.addLayer(all);
map.addLayer(markersdist100);
map.removeLayer(allmarkers);
});
});
//all data
var all = L.geoJson(data, {
pointToLayer: style_markers,
});
});
// FUNCTIONS
function filter_distance(feature, other) {
return feature.properties.distance < 100;
};
function getDistanceFromLatLonInKm(lat1, lon1, lat2, lon2) {
var R = 6371; // Radius of the earth in km
var dLat = deg2rad(lat2 - lat1); // deg2rad below
var dLon = deg2rad(lon2 - lon1);
var a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c; // Distance in km
return d;
};
function deg2rad(deg) {
return deg * (Math.PI / 180)
};
A screenshot of my map
Hope this can help others in their use cases
Feel free to optimize the code and share it with us

how to create a custom filter by passing a color code in fabric js

I am trying to create a custom filter it should like accepting color code.
Here is my code.
It is working fine.
fabric.Image.fromURL('pug.jpg', function(img) {
img.filters.push(
new fabric.Image.filters.Sepia(),
new fabric.Image.filters.Brightness({ brightness: 100 }));
img.applyFilters(canvas.renderAll.bind(canvas));
canvas.add(img);
});
Now, i need to create a filter with specific color code.
What i found is
fabric.Image.filters.Redify = fabric.util.createClass({
type: 'Redify',
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data;
for (var i = 0, len = data.length; i < len; i += 4) {
data[i + 1] = 0;
data[i + 2] = 0;
}
context.putImageData(imageData, 0, 0);
}
});
fabric.Image.filters.Redify.fromObject = function(object) {
return new fabric.Image.filters.Redify(object);
};
I need explanation what for loop does...also please explain how can i pass color code.
The redify filter you found is not really a colorify filter. As you can see from the code it is killing the green and blue channell and leaving you just with the reds of the image. It is not the same effect you would get applying a colorify with red.
You could create a bluify and greenify filter in the same way just changing the surviving channell:
fabric.Image.filters.Greenify= fabric.util.createClass({
type: 'greenify',
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data;
for (var i = 0, len = data.length; i < len; i += 4) {
//kill red
data[i] = 0;
//kill blue
data[i + 2] = 0;
}
context.putImageData(imageData, 0, 0);
}
});
To create a colorify filter, first you have to know how to do it. I personally checked how colorify filter from GIMP works:
https://docs.gimp.org/en/plug-in-colorify.html
1) make the image grayscale, based on luminosity
2) multiply the gray level for the color you want
This would be more or less equal to apply the existing fabricjs filters in order Grayscale and Multiply.
var canvas = new fabric.Canvas("c");
fabric.Image.filters.Luminosity = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** #lends fabric.Image.filters.Luminosity.prototype */ {
/**
* Filter type
* #param {String} type
* #default
*/
type: 'Luminosity',
/**
* Applies filter to canvas element
* #memberOf fabric.Image.filters.Grayscale.prototype
* #param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
len = imageData.width * imageData.height * 4,
index = 0,
average;
while (index < len) {
//Luminosity = 0.21 × R + 0.72 × G + 0.07 × B
average = (0.21 * data[index] + 0.72 * data[index + 1] + 0.07 * data[index + 2]);
data[index] = average;
data[index + 1] = average;
data[index + 2] = average;
index += 4;
}
context.putImageData(imageData, 0, 0);
}
});
/**
* Returns filter instance from an object representation
* #static
* #return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale
*/
fabric.Image.filters.Grayscale.fromObject = function() {
return new fabric.Image.filters.Grayscale();
};
fabric.Image.fromURL("http://fabricjs.com/assets/pug.jpg", function(img) {
img.filters.push(new fabric.Image.filters.Grayscale());
img.filters.push(new fabric.Image.filters.Multiply({color: '#F0F'}));
img.scale(0.3);
img.applyFilters(function() {
canvas.add(img);
});
}, {crossOrigin: 'Anonymous'});
fabric.Image.fromURL("http://fabricjs.com/assets/pug.jpg", function(img) {
img.filters.push(new fabric.Image.filters.Luminosity());
img.filters.push(new fabric.Image.filters.Multiply({color: '#F0F'}));
img.applyFilters(function() {
img.scale(0.3);
img.left = img.getWidth();
canvas.add(img);
});
}, {crossOrigin: 'Anonymous'});
<script src="http://www.deltalink.it/andreab/fabric/fabric.js"></script>
<canvas width="500" height="400" id="c" ></canvas>
To compare the built in functions of fabricjs with the example from gimp, i created a Luminosity filter to use instead of grayscale filter that is based on the "Average" method. As you see the results are pretty similar but it is image dependent.
Check the Multiply Filter source code to see how a parameter in the filter is handled if you want to build your own filter.
To be able to use your filter you need to change your filter code to below according to Fabricjs filter boilerplate
(function(global) {
'use strict';
var fabric = global.fabric || (global.fabric = { }),
filters = fabric.Image.filters,
createClass = fabric.util.createClass;
/**
* Redify filter class
* #class fabric.Image.filters.Redify
* #memberOf fabric.Image.filters
* #extends fabric.Image.filters.BaseFilter
* #see {#link fabric.Image.filters.Redify#initialize} for constructor definition
* #see {#link http://fabricjs.com/image-filters|ImageFilters demo}
* #example
* var filter = new fabric.Image.filters.Redify({
* add here an example of how to use your filter
* });
* object.filters.push(filter);
* object.applyFilters();
*/
filters.Redify = createClass(filters.BaseFilter, /** #lends fabric.Image.filters.Redify.prototype */ {
/**
* Filter type
* #param {String} type
* #default
*/
type: 'Redify',
/**
* Fragment source for the threshold program
*/
fragmentSource: 'precision highp float;\n' +
'uniform sampler2D uTexture;\n' +
'uniform float uthreshold;\n' +
'varying vec2 vTexCoord;\n' +
'void main() {\n' +
'vec4 color = texture2D(uTexture, vTexCoord);\n' +
// add your gl code here
'gl_FragColor = color;\n' +
'}',
/**
* Redify value, from -1 to 1.
* translated to -255 to 255 for 2d
* 0.0039215686 is the part of 1 that get translated to 1 in 2d
* #param {Number} threshold
* #default
*/
threshold: 5,
/**
* Describe the property that is the filter parameter
* #param {String} m
* #default
*/
mainParameter: 'threshold',
/**
* Apply the Redify operation to a Uint8ClampedArray representing the pixels of an image.
*
* #param {Object} options
* #param {ImageData} options.imageData The Uint8ClampedArray to be filtered.
*/
applyTo2d: function(options) {
var imageData = options.imageData,
data = imageData.data, i, len = data.length,sublim = 255-this.threshold;
for (i = 0; i < len; i += 4) {
if (data[i] < sublim && data[i + 1] < sublim && data[i + 2] < sublim) {
data[i + 1] = 0;
data[i + 2] = 0;
}
}
},
/**
* Return WebGL uniform locations for this filter's shader.
*
* #param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader.
* #param {WebGLShaderProgram} program This filter's compiled shader program.
*/
getUniformLocations: function(gl, program) {
return {
uMyParameter: gl.getUniformLocation(program, 'uMyParameter'),
};
},
/**
* Send data from this filter to its shader program's uniforms.
*
* #param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader.
* #param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects
*/
sendUniformData: function(gl, uniformLocations) {
gl.uniform1f(uniformLocations.uMyParameter, this.threshold);
},
});
/**
* Returns filter instance from an object representation
* #static
* #param {Object} object Object to create an instance from
* #param {function} [callback] to be invoked after filter creation
* #return {fabric.Image.filters.Redify} Instance of fabric.Image.filters.Redify
*/
fabric.Image.filters.Redify.fromObject = fabric.Image.filters.BaseFilter.fromObject;
})(typeof exports !== 'undefined' ? exports : this);
Once you have done that you can simply use it as below to pass a variable
fabric.Image.fromURL('pug.jpg', function(img) {
img.filters.push(
new fabric.Image.filters.Redify({ threshold: 10 }));
img.applyFilters();
canvas.add(img);
});

How to style editable circle controls in Google Maps

Does anyone know of an easy way to style the user controls (white dots on the bouns of the radius) for the editable circles in Google Maps? I cant find any way of doing this in the API documentation. At the very least it would be great to have the dots on a lower zindex than my pin but modify the zindex options of both has no results.
One option would be to do something like this example from the "article": Fun with MVC in the Google Maps Javascript API v3 documentation/ The handles there are just markers, the distance from the center is bound to the raduis, the center marker is bound to the center of the circle.
example fiddle
function init() {
var mapDiv = document.getElementById('map-canvas');
var map = new google.maps.Map(mapDiv, {
center: new google.maps.LatLng(37.790234970864, -122.39031314844),
zoom: 8,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
var distanceWidget = new DistanceWidget(map);
google.maps.event.addListener(distanceWidget, 'distance_changed', function() {
displayInfo(distanceWidget);
});
google.maps.event.addListener(distanceWidget, 'position_changed', function() {
displayInfo(distanceWidget);
});
}
google.maps.event.addDomListener(window, 'load', init);
Modified DistanceWidget code (changed the marker icons):
/**
* A distance widget that will display a circle that can be resized and will
* provide the radius in km.
*
* #param {google.maps.Map} map The map on which to attach the distance widget.
*
* #constructor
*/
function DistanceWidget(map) {
this.set('map', map);
this.set('position', map.getCenter());
var marker = new google.maps.Marker({
draggable: true,
icon: {
url: "https://maps.gstatic.com/intl/en_us/mapfiles/markers2/measle_blue.png",
size: new google.maps.Size(7, 7),
anchor: new google.maps.Point(4, 4)
},
title: 'Move me!'
});
// Bind the marker map property to the DistanceWidget map property
marker.bindTo('map', this);
// Bind the marker position property to the DistanceWidget position
// property
marker.bindTo('position', this);
// Create a new radius widget
var radiusWidget = new RadiusWidget();
// Bind the radiusWidget map to the DistanceWidget map
radiusWidget.bindTo('map', this);
// Bind the radiusWidget center to the DistanceWidget position
radiusWidget.bindTo('center', this, 'position');
// Bind to the radiusWidgets' distance property
this.bindTo('distance', radiusWidget);
// Bind to the radiusWidgets' bounds property
this.bindTo('bounds', radiusWidget);
}
DistanceWidget.prototype = new google.maps.MVCObject();
/**
* A radius widget that add a circle to a map and centers on a marker.
*
* #constructor
*/
function RadiusWidget() {
var circle = new google.maps.Circle({
strokeWeight: 2
});
// Set the distance property value, default to 50km.
this.set('distance', 50);
// Bind the RadiusWidget bounds property to the circle bounds property.
this.bindTo('bounds', circle);
// Bind the circle center to the RadiusWidget center property
circle.bindTo('center', this);
// Bind the circle map to the RadiusWidget map
circle.bindTo('map', this);
// Bind the circle radius property to the RadiusWidget radius property
circle.bindTo('radius', this);
this.addSizer_();
}
RadiusWidget.prototype = new google.maps.MVCObject();
/**
* Update the radius when the distance has changed.
*/
RadiusWidget.prototype.distance_changed = function () {
this.set('radius', this.get('distance') * 1000);
};
/**
* Add the sizer marker to the map.
*
* #private
*/
RadiusWidget.prototype.addSizer_ = function () {
var sizer = new google.maps.Marker({
draggable: true,
icon: {
url: "https://maps.gstatic.com/intl/en_us/mapfiles/markers2/measle_blue.png",
size: new google.maps.Size(7, 7),
anchor: new google.maps.Point(4, 4)
},
title: 'Drag me!'
});
sizer.bindTo('map', this);
sizer.bindTo('position', this, 'sizer_position');
var me = this;
google.maps.event.addListener(sizer, 'drag', function () {
// Set the circle distance (radius)
me.setDistance();
});
};
/**
* Update the center of the circle and position the sizer back on the line.
*
* Position is bound to the DistanceWidget so this is expected to change when
* the position of the distance widget is changed.
*/
RadiusWidget.prototype.center_changed = function () {
var bounds = this.get('bounds');
// Bounds might not always be set so check that it exists first.
if (bounds) {
var lng = bounds.getNorthEast().lng();
// Put the sizer at center, right on the circle.
var position = new google.maps.LatLng(this.get('center').lat(), lng);
this.set('sizer_position', position);
}
};
/**
* Calculates the distance between two latlng locations in km.
* #see http://www.movable-type.co.uk/scripts/latlong.html
*
* #param {google.maps.LatLng} p1 The first lat lng point.
* #param {google.maps.LatLng} p2 The second lat lng point.
* #return {number} The distance between the two points in km.
* #private
*/
RadiusWidget.prototype.distanceBetweenPoints_ = function (p1, p2) {
if (!p1 || !p2) {
return 0;
}
var R = 6371; // Radius of the Earth in km
var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c;
return d;
};
/**
* Set the distance of the circle based on the position of the sizer.
*/
RadiusWidget.prototype.setDistance = function () {
// As the sizer is being dragged, its position changes. Because the
// RadiusWidget's sizer_position is bound to the sizer's position, it will
// change as well.
var pos = this.get('sizer_position');
var center = this.get('center');
var distance = this.distanceBetweenPoints_(center, pos);
// Set the distance property for any objects that are bound to it
this.set('distance', distance);
};
function displayInfo(widget) {
var info = document.getElementById('info');
info.innerHTML = 'Position: ' + widget.get('position').toUrlValue(3) + ', distance: ' + widget.get('distance').toFixed(3);
}
code snippet:
function init() {
var mapDiv = document.getElementById('map-canvas');
var map = new google.maps.Map(mapDiv, {
center: new google.maps.LatLng(37.790234970864, -122.39031314844),
zoom: 8,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
var distanceWidget = new DistanceWidget(map);
google.maps.event.addListener(distanceWidget, 'distance_changed', function() {
displayInfo(distanceWidget);
});
google.maps.event.addListener(distanceWidget, 'position_changed', function() {
displayInfo(distanceWidget);
});
}
google.maps.event.addDomListener(window, 'load', init);
/**
* A distance widget that will display a circle that can be resized and will
* provide the radius in km.
*
* #param {google.maps.Map} map The map on which to attach the distance widget.
*
* #constructor
*/
function DistanceWidget(map) {
this.set('map', map);
this.set('position', map.getCenter());
var marker = new google.maps.Marker({
draggable: true,
icon: {
url: "https://maps.gstatic.com/intl/en_us/mapfiles/markers2/measle_blue.png",
size: new google.maps.Size(7, 7),
anchor: new google.maps.Point(4, 4)
},
title: 'Move me!'
});
// Bind the marker map property to the DistanceWidget map property
marker.bindTo('map', this);
// Bind the marker position property to the DistanceWidget position
// property
marker.bindTo('position', this);
// Create a new radius widget
var radiusWidget = new RadiusWidget();
// Bind the radiusWidget map to the DistanceWidget map
radiusWidget.bindTo('map', this);
// Bind the radiusWidget center to the DistanceWidget position
radiusWidget.bindTo('center', this, 'position');
// Bind to the radiusWidgets' distance property
this.bindTo('distance', radiusWidget);
// Bind to the radiusWidgets' bounds property
this.bindTo('bounds', radiusWidget);
}
DistanceWidget.prototype = new google.maps.MVCObject();
/**
* A radius widget that add a circle to a map and centers on a marker.
*
* #constructor
*/
function RadiusWidget() {
var circle = new google.maps.Circle({
strokeWeight: 2
});
// Set the distance property value, default to 50km.
this.set('distance', 50);
// Bind the RadiusWidget bounds property to the circle bounds property.
this.bindTo('bounds', circle);
// Bind the circle center to the RadiusWidget center property
circle.bindTo('center', this);
// Bind the circle map to the RadiusWidget map
circle.bindTo('map', this);
// Bind the circle radius property to the RadiusWidget radius property
circle.bindTo('radius', this);
this.addSizer_();
}
RadiusWidget.prototype = new google.maps.MVCObject();
/**
* Update the radius when the distance has changed.
*/
RadiusWidget.prototype.distance_changed = function() {
this.set('radius', this.get('distance') * 1000);
};
/**
* Add the sizer marker to the map.
*
* #private
*/
RadiusWidget.prototype.addSizer_ = function() {
var sizer = new google.maps.Marker({
draggable: true,
icon: {
url: "https://maps.gstatic.com/intl/en_us/mapfiles/markers2/measle_blue.png",
size: new google.maps.Size(7, 7),
anchor: new google.maps.Point(4, 4)
},
title: 'Drag me!'
});
sizer.bindTo('map', this);
sizer.bindTo('position', this, 'sizer_position');
var me = this;
google.maps.event.addListener(sizer, 'drag', function() {
// Set the circle distance (radius)
me.setDistance();
});
};
/**
* Update the center of the circle and position the sizer back on the line.
*
* Position is bound to the DistanceWidget so this is expected to change when
* the position of the distance widget is changed.
*/
RadiusWidget.prototype.center_changed = function() {
var bounds = this.get('bounds');
// Bounds might not always be set so check that it exists first.
if (bounds) {
var lng = bounds.getNorthEast().lng();
// Put the sizer at center, right on the circle.
var position = new google.maps.LatLng(this.get('center').lat(), lng);
this.set('sizer_position', position);
}
};
/**
* Calculates the distance between two latlng locations in km.
* #see http://www.movable-type.co.uk/scripts/latlong.html
*
* #param {google.maps.LatLng} p1 The first lat lng point.
* #param {google.maps.LatLng} p2 The second lat lng point.
* #return {number} The distance between the two points in km.
* #private
*/
RadiusWidget.prototype.distanceBetweenPoints_ = function(p1, p2) {
if (!p1 || !p2) {
return 0;
}
var R = 6371; // Radius of the Earth in km
var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c;
return d;
};
/**
* Set the distance of the circle based on the position of the sizer.
*/
RadiusWidget.prototype.setDistance = function() {
// As the sizer is being dragged, its position changes. Because the
// RadiusWidget's sizer_position is bound to the sizer's position, it will
// change as well.
var pos = this.get('sizer_position');
var center = this.get('center');
var distance = this.distanceBetweenPoints_(center, pos);
// Set the distance property for any objects that are bound to it
this.set('distance', distance);
};
function displayInfo(widget) {
var info = document.getElementById('info');
info.innerHTML = 'Position: ' + widget.get('position').toUrlValue(3) + ', distance: ' + widget.get('distance').toFixed(3);
}
html,
body,
#map-canvas {
height: 100%;
margin: 0px;
padding: 0px
}
<script src="https://maps.googleapis.com/maps/api/js?v=3&libraries=geometry"></script>
<div id="info"></div>
<div id="map-canvas"></div>
I'm afraid you can't. The API provides no control over those handles, and since the circles live inside a canvas, there's no easy way to identify the element in order to style it.
Edit:
You can declare your circle as non-editable and allow edits only on mouseover.
var circle=new google.maps.Circle({
center: map.getCenter(),
map: map,
editable: false
});
google.maps.event.addListener(circle, 'mouseover', function () {
circle.set('editable',true);
});
google.maps.event.addListener(circle, 'mouseout', function () {
circle.set('editable',false);
});
One simple way of adding style to the edit pointer is as follows.
#map div[style^="position: absolute; left: 0px; top: 0px; width: 9px; height: 9px;"] {
border-color: #777575 !important;
border-width: 2px !important;
}

Toggling Google map circle Hide and UnHide

I want to hide and show Google map circle. Hope the API has provided any simple method but I am unable to figure it out.
And can anyone let me know how to place the circle to left corner of the map?
Waiting for your help, here is my code:
/**
* A distance widget that will display a circle that can be resized and will
* provide the radius in km.
*
* #param {google.maps.Map} map The map to attach to.
*
* #constructor
*/
function DistanceWidget(map) {
this.set('map', map);
this.set('position', map.getCenter());
var marker = new google.maps.Marker({
draggable: true,
title: 'Move me!'
});
// Bind the marker map property to the DistanceWidget map property
marker.bindTo('map', this);
// Bind the marker position property to the DistanceWidget position
// property
marker.bindTo('position', this);
// Create a new radius widget
var radiusWidget = new RadiusWidget();
// Bind the radiusWidget map to the DistanceWidget map
radiusWidget.bindTo('map', this);
// Bind the radiusWidget center to the DistanceWidget position
radiusWidget.bindTo('center', this, 'position');
// Bind to the radiusWidgets' distance property
this.bindTo('distance', radiusWidget);
// Bind to the radiusWidgets' bounds property
this.bindTo('bounds', radiusWidget);
}
DistanceWidget.prototype = new google.maps.MVCObject();
/**
* A radius widget that add a circle to a map and centers on a marker.
*
* #constructor
*/
function RadiusWidget() {
var circle = new google.maps.Circle({
strokeWeight: 2
});
// Set the distance property value, default to 50km.
this.set('distance', 50);
// Bind the RadiusWidget bounds property to the circle bounds property.
this.bindTo('bounds', circle);
// Bind the circle center to the RadiusWidget center property
circle.bindTo('center', this);
// Bind the circle map to the RadiusWidget map
circle.bindTo('map', this);
// Bind the circle radius property to the RadiusWidget radius property
circle.bindTo('radius', this);
// Add the sizer marker
this.addSizer_();
}
RadiusWidget.prototype = new google.maps.MVCObject();
/**
* Update the radius when the distance has changed.
*/
RadiusWidget.prototype.distance_changed = function () {
this.set('radius', this.get('distance') * 1000);
};
/**
* Add the sizer marker to the map.
*
* #private
*/
RadiusWidget.prototype.addSizer_ = function () {
var sizer = new google.maps.Marker({
draggable: true,
title: 'Drag me!'
});
sizer.bindTo('map', this);
sizer.bindTo('position', this, 'sizer_position');
var me = this;
google.maps.event.addListener(sizer, 'drag', function () {
// Set the circle distance (radius)
me.setDistance();
});
};
/**
* Update the center of the circle and position the sizer back on the line.
*
* Position is bound to the DistanceWidget so this is expected to change when
* the position of the distance widget is changed.
*/
RadiusWidget.prototype.center_changed = function () {
var bounds = this.get('bounds');
// Bounds might not always be set so check that it exists first.
if (bounds) {
var lng = bounds.getNorthEast().lng();
// Put the sizer at center, right on the circle.
var position = new google.maps.LatLng(this.get('center').lat(), lng);
this.set('sizer_position', position);
}
};
/**
* Calculates the distance between two latlng points in km.
* #see http://www.movable-type.co.uk/scripts/latlong.html
*
* #param {google.maps.LatLng} p1 The first lat lng point.
* #param {google.maps.LatLng} p2 The second lat lng point.
* #return {number} The distance between the two points in km.
* #private
*/
RadiusWidget.prototype.distanceBetweenPoints_ = function (p1, p2) {
if (!p1 || !p2) {
return 0;
}
var R = 6371; // Radius of the Earth in km
var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c;
return d;
};
/**
* Set the distance of the circle based on the position of the sizer.
*/
RadiusWidget.prototype.setDistance = function () {
// As the sizer is being dragged, its position changes. Because the
// RadiusWidget's sizer_position is bound to the sizer's position, it will
// change as well.
var pos = this.get('sizer_position');
var center = this.get('center');
var distance = this.distanceBetweenPoints_(center, pos);
// Set the distance property for any objects that are bound to it
this.set('distance', distance);
};
function init() {
var mapDiv = document.getElementById('map-canvas');
var map = new google.maps.Map(mapDiv, {
center: new google.maps.LatLng(37.790234970864, -122.39031314844),
zoom: 8,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
var distanceWidget = new DistanceWidget(map);
google.maps.event.addListener(distanceWidget, 'distance_changed', function () {
displayInfo(distanceWidget);
});
google.maps.event.addListener(distanceWidget, 'position_changed', function () {
displayInfo(distanceWidget);
});
}
function displayInfo(widget) {
var info = document.getElementById('info');
info.innerHTML = 'Position: ' + widget.get('position') + ', distance: ' + widget.get('distance');
}
google.maps.event.addDomListener(window, 'load', init);
Changed this code (made "map" and "distanceWidget" global):
var map = null;
var distanceWidget = null;
function init() {
var mapDiv = document.getElementById('map-canvas');
map = new google.maps.Map(mapDiv, {
center: new google.maps.LatLng(37.790234970864, -122.39031314844),
zoom: 8,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
distanceWidget = new DistanceWidget(map);
google.maps.event.addListener(distanceWidget, 'distance_changed', function () {
displayInfo(distanceWidget);
});
google.maps.event.addListener(distanceWidget, 'position_changed', function () {
displayInfo(distanceWidget);
});
}
function displayInfo(widget) {
var info = document.getElementById('info');
info.innerHTML = 'Position: ' + widget.get('position') + ', distance: ' + widget.get('distance');
}
Added this to the HTML, hides and shows the distanceWidget:
<button onclick="distanceWidget.set('map',null);" >Hide</button>
<button onclick="distanceWidget.set('map',map);">Show</button>
<div id="info"></div>
<div id="map-canvas"></div>
working fiddle

bing maps: how to set zoom level so pinpoint is visible to users current location

I am using bing maps ajax v7 and for simplicity's sake let's say I have 10 PinPoints placed around the world. I'm trying to have the map zoom to the lowest level so that the closest PinPoint is still visible to the current location of the user. If someone could point me in the right direction I would greatly appreciate it.
$(document).ready(function () {
var windowHeight = $(window).height();
var windowWidth = $(window).width();
map = new Microsoft.Maps.Map(document.getElementById("mapDiv"), {
credentials: "myCredentials",
backgroundColor: "#A4C4ED",
zoom: 3,
height: windowHeight,
width: windowWidth
});
Microsoft.Maps.Events.addHandler(map, 'viewchange', hideInfoBox);
Microsoft.Maps.Events.addHandler(map, 'click', hideInfoBox);
//get users location and set view bound
var geoLocationProvider = new Microsoft.Maps.GeoLocationProvider(map);
var viewRectangle = Microsoft.Maps.LocationRect(geoLocationProvider.getCurrentPosition());
map.setView({ bounds: viewRectangle });
dataLayer = new Microsoft.Maps.EntityCollection();
map.entities.push(dataLayer);
var infoboxLayer = new Microsoft.Maps.EntityCollection();
map.entities.push(infoboxLayer);
//create initial infobox
infobox = new Microsoft.Maps.Infobox(new Microsoft.Maps.Location(0, 0), {
visible: false,
offset: new Microsoft.Maps.Point(0, 20)
});
infoboxLayer.push(infobox);
Microsoft.Maps.loadModule('Microsoft.Maps.Search', { callback: searchModuleLoaded });
});
I assume that you is one pin point and you have another 10 pin points located somewere on the map.
First you need to find the pinpoint that is closest to you.
You can use this function that expect two location objects that contains latitude and longitude.
oLocation1 = {latitude:0,longitude:0};
oLocation2 = {latitude:19,longitude:23};
function calcDistHaversine (oLocation1, oLocation2) {
var dLat = (oLocation2.latitude * Math.PI / 180 - oLocation1.latitude * Math.PI / 180);//*Math.PI*180;
var dLon = (oLocation2.longitude * Math.PI / 180 - oLocation1.longitude * Math.PI / 180);//*Math.PI*180;
var lat1 = oLocation1.latitude * Math.PI / 180;//*Math.PI*180;
var lat2 = oLocation2.latitude * Math.PI / 180;//*Math.PI*180;
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var distance = 6371 * c;
return distance;
};
As a result you will get the distance between those two location with respect yo earth curvature.
Now you have your location and closest pinpoint location.
Lets name them as your,their.
Next you need to create array that contains those two location converted to microsoft location objects.
var yourLocation= new Microsoft.Maps.Location(your.latitude, your.longitude);
var theirLocation= new Microsoft.Maps.Location(their.latitude, their.longitude);
var arr = [];
arr.push(yourLocation);
arr.push(theirLocation);
Now you use bing maps feature that gives you best zoom and pointing according to given locations.
var bestView = Microsoft.Maps.LocationRect.fromLocations(arrLocations);
Then you set the map view according to the best view that we found.
setTimeout((function () {
map.setView({ bounds: bestView });
}).bind(this), 1000);

Categories

Resources