I am trying to create a weighted heat map for my android app project. I had a look at the google documentation for it. I don't understand how to create a new gradient using colors array and starting points array. The starting array is denoted as
The starting point for each color, given as a percentage of the maximum intensity.
What does it mean? How to relate color array with starting point array?
int[] colors = {
Color.GREEN, // green(0-50)
Color.YELLOW, // yellow(51-100)
Color.rgb(255,165,0), //Orange(101-150)
Color.RED, //red(151-200)
Color.rgb(153,50,204), //dark orchid(201-300)
Color.rgb(165,42,42) //brown(301-500)
};
float[] startpoints = {
};
I need to fill this startpoints array.
Here are some assumptions:
the gradient colorMapSize is 1000 (default) but should be set to 500
the color values length is 6
the startPoints length is 6 (has to be the same length as the colors)
the colors array is specified in OP
the startingPoints array should be { 0.1F, 0.2F, 0.3F, 0.4F, 0.6F, 1.0F }
Here's a diagram to help the discussion:
ColorMap
The first thing to understand is the colorMap. This is generated by the
Gradient builder. It is an 'int' array with size by default of 1000 but is
customizable in one of the constructors - recommend 500 in your example. So every value in this array is a color value. (The size affects the resolution of your gradient.)
Color values in the colorMap are affected by 2 controls which produce color
intervals: the startPoints array and the colors array. In your example there
is 6 color intervals.
A color interval is defined as a starting color and end color and the number
of 'colorMap' slots in range. All colorMap values for any given interval
are interpolated using the start/end colors for that range.
If the first value of the startPoints array is 0 then the first color interval
is assumed to be solid - non-zero implies a transition from transparent to the first color which seems most desirable. (See example where the startPoints is set to 0 and notice the jaggedness of the outside areas.)
If the first value of the startPoints array is not 0 then the first color
interval is defined as starting with the first color (colors[0]) and a range
of the colorMapSize * the first starting point, e.g. 500 * 0.1 = 50
and ending with the same color.
For all remaining defined colors an interval is generated - again with a
starting colorMap slot, a starting color (which is the previous color end),
and ending color (which is the current color in the table) and a range.
If the last startingPoint is not 1.0, then the last color is used for start
and stop. In this example, 1.0 is probably best.
(Opacity applies to the whole layer and simply affects the alpha channel of the color.)
Tiles
This is where 'intensity' is appreciated and the effect a WeightedLatLng can play a part.
When the map is generated it divides the viewable area into tiles - the
number of tiles is a function of zoom level. At zoom level 0 there is 1 tile,
and the arbitrary zoom level tile count is 2^n (where n is the zoom level).
Each tile is further divided into buckets. The number of buckets is a function
of zoom level and Radius (blur). Each bucket corresponds to a geographical
area within the tile - think rectangle-grid.
The points inside the current tile are obtained from the data set.
For all of the points within the geographic bounds of the tile, the intensity value of the point is added to its corresponding bucket. The intensity value for a point by default is 1. The WeightedLatLng allows you to bias a point by changing this value from 1 to some number (larger to increase importance, smaller to decrease importance). So the result is the bucket intensity tally is affected from what it would be for just LatLngs.
Once the intensity buckets are tallied, the intensity values are colorized using
the colorMap determined in the first section. Note that the range of intensity values are scaled to the size of the colorMap such that the maximum intensity value maps to the last color.
The buckets are then applied to the tile and the tile is made into a bitmap
and is rendered.
Example
For an example I used some data on crime in Sacramento. I first created a non-weighted heatmap. And then created a weighted heatmap to give an importance to auto-thefts by specifying an intensity of 5.0 (verses the default of 1.0 for any point) for crimes with an NCIC code of 2404 (vehicle-theft). The 5.0 is somewhat arbitrary and really depends on the data and what you're trying to convey.
Non-Weighted / Weighted (by vehicle theft)
And an example where the first startPoint[0] is 0.0 - which shows the lack of a transition from transparent to initial color:
Here are the relevant portions of the MapActivity:
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
mMap.getUiSettings().setZoomControlsEnabled(true);
// Add a marker in Sydney and move the camera
LatLng sydney = new LatLng(-34, 151);
mMap.addMarker(new MarkerOptions().position(sydney).title("Marker in Sydney"));
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney));
addHeatMapWeighted();
//addHeatMap();
}
public void addHeatMapWeighted() {
Gradient gradient = new Gradient(colors,startpoints);
List<WeightedLatLng> wDat = new CrimeData().getWeightedPositions();
HeatmapTileProvider provider = new HeatmapTileProvider.Builder().weightedData(wDat).gradient(gradient).build();
mMap.addTileOverlay(new TileOverlayOptions().tileProvider(provider));
}
public void addHeatMap() {
Gradient gradient = new Gradient(colors,startpoints);
List<LatLng> cDat = new CrimeData().getPositions();
HeatmapTileProvider provider = new HeatmapTileProvider.Builder().data(cDat).gradient(gradient).build();
mMap.addTileOverlay(new TileOverlayOptions().tileProvider(provider));
}
int[] colors = {
Color.GREEN, // green(0-50)
Color.YELLOW, // yellow(51-100)
Color.rgb(255,165,0), //Orange(101-150)
Color.RED, //red(151-200)
Color.rgb(153,50,204), //dark orchid(201-300)
Color.rgb(165,42,42) //brown(301-500)
};
float[] startpoints = {
0.1F, 0.2F, 0.3F, 0.4F, 0.6F, 1.0F
};
Start of Interval
OK, so you may have noticed that the start of each colorMap interval starts at a nice round number (0,50,100...) which doesn't quite match your requirements (51, 101...301). I would argue that your commented-ranges are not right since really that means there are 501 possible colors which is a bit odd. But if you really wanted that range as specified then you'd have to do some math on to come up with an alternate startPoints array of: (51/501,101/501,151/501,201/501,301/501,501/501) = (.101796407,.201596806,.301397206,.401197605,.600798403, 1.0)
Radius
The radius value is an input to the HeatMap's Gaussian Blur implementation. Sometimes a picture is the best explanation: this is an animated Gif which cycles through a HeatMap of radius 20 to 50 in steps of 10 (with the blurriest being radius 50).
Since the heat map is intended to convey meaning to the information, it's really left to the data presenter to assess what is the best radius effect. So for example, in the case of crime data, as a consumer of the data looking for a place to live, I'd probably gain more value from the data with some blur. On the other hand if the data was presenting fire stations then too much blur could very well lead one to believe they are covered when they are not.
Dots
Just using dots (circles) tells a more accurate story than the heat map and which at the broader zoom levels visually blurs just like a heat map albeit without the color. For example, here are two dot renderings, unweighted and weighted. The weighted bias is for drug crimes (NCIC codes [3500,3600)) which shows that drug crimes predominate this area.
HeatMap Trivia
From the referenced book (3):
The term "heat map" was trademarked in 1991 by software developer
Cormac Kinney. It was then acquired by SS&C Technologies, Inc. but
they did not extend the license and it was annulled in 2006.
References
Crime data: https://support.spatialkey.com/spatialkey-sample-csv-data/
Android code:
https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/heatmaps/
HeatMap trademark: https://books.google.com/books?id=clIWDAAAQBAJ&pg=PA219&lpg=PA219&dq=is+heatmap+trademarked&source=bl&ots=XfcTsOc2pG&sig=EPaHG95M8uXiI5WAAwpa-e3zHXE&hl=en&sa=X&ved=0ahUKEwijtKSl2IfaAhUNy1kKHdBUAdsQ6AEIlwEwCg#v=onepage&q=is%20heatmap%20trademarked&f=false
Related
How to remove border color from android google heatmap? I'm using this code to draw:
fun drawPolygons(polygons: Array<NetworkMapPolygonModel>, scale: Float) {
map?.let { map ->
val points = ArrayList<WeightedLatLng>()
polygons.forEach {
val intensity = ((it.signalStrength - 12) * -1).toDouble()
points.add(WeightedLatLng(
LatLng(it.aLatitude, it.aLongitude), intensity
))
}
val radius = 40
val provider = HeatmapTileProvider.Builder()
.weightedData(points)
.radius(radius)
.maxIntensity(48.0)
.build()
map.addTileOverlay(TileOverlayOptions().tileProvider(provider))
}
}
But google map drawing me this map:
I want to remove outer green border (in red square in the screenshot). But cannot find how to do this.
Please help!
To simply eliminate the green (and therefore the representation of data) use the gradient property of the HeatmapFileProvider.Builder to change the initial color from green (default) to yellow and change the starting threshold from the threshold that corresponded to green (0.2 default) to approximately (0.4) (you'll have to experiment with this number - at the bottom of this answer I show how to determine this and it is 0.454). And rather than a gradual fade-in from transparent I'll show how to start fully opaque at the desired color.
Before diving into modifications, understand that there is a default gradient used without being specified and it is this:
// Create the gradient.
val colors = intArrayOf(
Color.rgb(120, 225, 0), // green
Color.rgb(255, 0, 0) // red
)
val startPoints = floatArrayOf(0.2f, 1f)
val gradient = Gradient(colors, startPoints)
// Create the tile provider.
val provider = HeatmapTileProvider.Builder()
.data(latLngs)
.gradient(gradient)
.build()
In what follows, the colors array and startPoints are modified to demonstrate the point for each adjustment. So in this snippet, it shows eliminating green but transitioning from transparent to yellow (not what you are looking for but just an example).
// Create the gradient.
val colors = intArrayOf(
Color.rgb(255, 225, 0), // yellow
Color.rgb(255, 0, 0) // red
)
val startPoints = floatArrayOf(0.4f, 1f)
val gradient = Gradient(colors, startPoints)
In this answer I've used my own data representing crime statistics in Sacremento. You'll soon see why green is a good choice for the edge data.
The gradient property is composed of two controls:
colors
starting points (0.0 to 1.0)
By default colors is (GREEN,RED) and starting points is (0.2, 1.0). One thing to note is if the first starting point is non-zero (as in the defaults), the gradient from pts 0.0 to 0.2 transition from transparent to the color; otherwise it starts with the first color at pt 0.0.
Image (A) is the default settings for my data.
I then wanted to see where the green actually started (0.2) without the transition from transparent to green (0.0 - 0.2). To do this I modified the gradient to be transparent up to close to 2.0 - then introduce a border (black) from close-to-2.0 to 2.0 and the rest is defaults.
// Create the gradient.
val colors = intArrayOf(
Color.argb(0, 0, 0, 0), // transparent
Color.argb(0, 0, 0, 0), // transparent
Color.rgb(0, 0, 0), // black
Color.rgb(120, 255, 0), // green
Color.rgb(255, 0, 0) // red
)
val startPoints = floatArrayOf(0.0f, 0.15f, 0.18f, 0.2f, 1f)
val gradient = Gradient(colors, startPoints)
Image (B) is this "border" added to show where the pure green data (2.0+) starts:
And now to address the issue of removing green. The default interpolation is summarized as follows: 0 - 0.2f (transparent to green) and 0.2f - 1.0f (green to red). So somewhere in there is the interpolated yellow. For this answer, an approximation is made that the yellow is roughly 0.4 (but I'll follow up with a calculation to show how to figure that out). Again I add the border to show exactly where the yellow (0.4) is starting:
// Create the gradient.
val colors = intArrayOf(
Color.argb(0, 0, 0, 0), // transparent
Color.argb(0, 0, 0, 0), // transparent
Color.rgb(0, 0, 0), // black
Color.rgb(255, 255, 0), // yellow
Color.rgb(255, 0, 0) // red
)
val startPoints = floatArrayOf(0.0f, 0.35f, 0.38f, 0.4f, 1f)
val gradient = Gradient(colors, startPoints)
This answer demonstrates how to control the color results; the more pertinent question which I cannot answer is what information are you trying to convey: eliminating the "green" is eliminating data, whereas turning the data into a transition from yellow data using default starting points would not eliminate data but just eliminate green. (I did not post an example of this but worth considering.)
(A)
(B)
(C)
This next part of the answer focuses on color; hopefully not too far off topic.
Again, in the default gradient there are 2 percentiles specified (0.2, 1.0) and one implied starting at 0.0. And again, the colors for these three are: (0x0078E100 (fully transparent green(120,225,0)), 0xFF78E100 (opaque green), 0xFFFF0000 (opaque red)).
In the OP the question is in terms of color ("remove green border") which leads to having to make an assumption: up to what point should the removal stop. I chose to assume yellow - but since the colors represent percentiles of data really the question should be phrased in terms of percentiles to be precise.) But looking at the data representation in terms of color presents a problem: Where is the data percentile for yellow given the default gradient.
So to help this discussion I created a simple TileProvider whose purpose is to display the gradient of color in each tile rendered. Here's a sample:
This image shows one full tile and two partial tiles at the top and bottom; so here focus on the one full tile in the middle.
Some key points: (1) the tile starts at fully transparent (data percentile 0.0) and transitions to the first color in the default gradient at which point a black line segment is drawn representing the 20th percentile. From there the tile transitions from green to the second color in the default gradient (red) representing the 100th percentile. Along the way a second black line segment is drawn for the color in the gradient "closest" to "yellow".
In order to discover the closest data percentile to yellow, some knowledge of how the gradient is created is necessary. In short, the endpoints of each color segment in RGB values of the colors provided () are converted to HSV values. From these HSL values the ratio between the start and end points is applied the HSV value and then converted back to RGB.
Once the RGB color within the gradient segment is determined it's "distance" to the target ("tgt") color (YELLOW) is computed and the minimum distance found:
int deltaR = Color.red(tgt) - Color.red(ic);
int deltaG = Color.green(tgt) - Color.green(ic);
int deltaB = Color.blue(tgt) - Color.blue(ic);
double d = Math.sqrt(deltaR*0.3F*deltaR*0.3F + deltaG*0.59F*deltaG*0.59F + deltaB*0.11*deltaB*0.11);
As it turns out the data percentile closest to yellow is 45.4%. So the data presented in the final image above (3) represents the upper 54.6% of the data.
For reference here is the implementation of the TileProvider used. The mColors array is the 1000 element colors map which the heat map generates by default:
private class MyTileProvider implements TileProvider {
public MyTileProvider() {
}
#Override
public Tile getTile(int x, int y, int zoom) {
Bitmap tile = Bitmap.createBitmap(512,512,Bitmap.Config.ARGB_8888);
tile.setPixels(mColors,0,Math.max(512,mPixelsPerColor),0,0,512,512);
Log.d(TAG,"Tile gen done: "+x+","+y+" "+zoom);
return convertBitmap(tile);
}
}
For reference on gradients and heat maps use this answer:
Weighted heat maps in android
For reference on computing "color distance": https://stackoverflow.com/a/1847112/2711811
For reference on the map utils heat map implementation (a subdirectory within the andoid-maps-utils repo): https://github.com/googlemaps/android-maps-utils/tree/ac9684d627905587b020a0eac301e94478804a48/library/src/main/java/com/google/maps/android/heatmaps
I'm using OpenCV on Android to find circles of specific colour's in real time. My first step is to keep only pixels which corresponds to my defined color i'm looking for (red or green in this example). Example Image.
For this purpose i'm using the method inRange().
Here is my Question: What kind of color model (RGB, BGR, HSV, ..) is required as lower-/upper-bound color parameter's? And: what is a good practice to define these color bounds in respect to natural brightness changes?
matRgba = inputFrame.rgba();
Scalar lowerColorBound = Scalar(0.0, 0.0, 0.0); // Blue, Green, Red?
Scalar upperColorBound = Scalar(0.0, 0.0, 0.0);
// convert to HSV, necessary to use inRange()
Imgproc.cvtColor(matRgba, matRgba, Imgproc.COLOR_RGB2HSV);
// keep only the pixels defined by lower and upper bound range
Core.inRange(matRgba, lowerColorBound, upperColorBound, matRgba);
The required color model for the inRange(src, lowerb, upperb, dst) function in OpenCV is HSV.
The lowerb and upperb parameters specify the required lower and upper color bounds in the HSV format. In OpenCV, for HSV, Hue range is [0,179], Saturation range is [0,255] and Value range is [0,255].
For object tracking applications a possible practice (as suggested in the official documentation) to define these two color bounds can be:
Start from a color to track in RGB format.
Convert the color to the HSV format. Let (H, S, V) be its value.
Assign the value (H - deltaH, minS, minV) to lowerb and the value (H - deltaH, maxS, maxV) to upperb.
Possible starting values for the parameters defined in step 3 can be:
deltaH = 10
minS = 100, minV = 100
maxS = 255, maxV = 255
Then you can adjust them to narrow down or enlarge the H, S, V intervals as needed.
I guess the title gives away most of my questions, but let's detail and give a bit of background:
I have an Android app focused mainly for tablets that will be displaying a few different real-time data in TimeCharts. So I already have a service to communicate with the data source that grabs the data, parse it and add the values to singletons TimeSeries. When the user navigates in and out of fragments the fragment simply adds the correct TimeSeries and keeps calling mChartView.repaint(); every 500ms
All that is operational and working nicely across tabs, rotation, notification, etc.
Because it's a tablet app I want to achieve two things: 1) maximize viewing area; 2) make it look good.
1) I already removed the zoom buttons (for some reason if I try to add them it gives me NullException) with mRenderer.setZoomButtonsVisible(false); but there's still a huge bottom margin taking a lot of screen space. I tried setting a negative border, and it works, but the labels don't move with this border and I end up with the X axis crossing over the labels or even passing them.
How can I move the labels down with the border, or maybe put them on the top of the graph?
2)
a) the app is user configurable to use either Holo_Dark or Holo_light. So I'm using a transparent background on the renderer (see snippet below) to show that nice background shading that holo does. How to a change the axis text color? I found the mRenderer.setAxesColor(color) but it only change the line, not the text.
int color = Color.argb(0, 255, 255, 255); // Transparent colour
mRenderer.setBackgroundColor(color);
mRenderer.setMarginsColor(color);
b) the Y axis is crossing on top of it's labels, how can I offset them a bit to the left so the text is readable? (it's not with setMargins())
c) when the user zoom and pan around the chart, the chart stops updating/re-zooming the latest values on the screen and the user have to keep panning to see the latest values. I added a 'Reset Zoom' button on the ActionBar, but I can't make it work. What would be the code to make the values update again.
d) if my TimeSeires have a range of 10min (2 values per second) how can I make it only show the last 1min (for example) and if the user want's previous values he can pan back? (and the use button on c) re-start)
at the moment my rendered is setup as:
int color = Color.argb(0, 255, 255, 255); // Transparent colour
mRenderer.setBackgroundColor(color);
mRenderer.setMarginsColor(color);
mRenderer.setAxisTitleTextSize(16); // 16
mRenderer.setChartTitleTextSize(20); // 20
mRenderer.setLabelsTextSize(15); // 15
mRenderer.setLegendTextSize(15); // 15
mRenderer.setPointSize(0); // 10
mRenderer.setMargins(new int[] { 20, 30, 15, 0 });
mRenderer.setZoomButtonsVisible(false);
mRenderer.setShowAxes(true);
mRenderer.setShowLegend(true);
mRenderer.setShowGridX(true);
mRenderer.setShowLabels(true);
and each rendered added to the Multiple renderer goes like:
renderer.setDisplayChartValues(false);
mRenderer.addSeriesRenderer(renderer);
thanks a lot for all help!
Editing just to include my final code for others to see:
I've ended up ditching the legend completely and implementing my own legend floating above the chart, so the layout is a FrameLayout with a ChartView and a TableLayout.
I implemented an abstract CurvesFragment Class that is extended by a few classes.
I configure the chart during my fragment OnCreateView as:
mChartView = ChartFactory.getTimeChartView(getActivity().getApplicationContext(), mDataset, mRenderer, "HH:mm:ss"); // X axis in a time scale
// Setup chart renderer =============================
mRenderer.setLabelsTextSize(15); // Text size
mRenderer.setMargins(new int[] { 0, Utils.convertToPx(5, getActivity().getApplicationContext()), 0, 0 }); // 5dp on the left to show that little line
mRenderer.setZoomButtonsVisible(false); // bye bye zoom
mRenderer.setShowAxes(true); // show both axes
mRenderer.setShowLegend(false); // bye bye legend
mRenderer.setShowGridX(true); // X grid helps identify values
mRenderer.setShowLabels(true); // See the values
mRenderer.setYLabelsAlign(Align.LEFT); // put the Y labels on the left of the axis
if (Utils.getCurrentTheme(getActivity().getApplicationContext()) == android.R.style.Theme_Holo) {
mRenderer.setXLabelsColor(getResources().getColor(android.R.color.primary_text_dark));
mRenderer.setYLabelsColor(0, getResources().getColor(android.R.color.primary_text_dark));
mRenderer.setMarginsColor(getResources().getColor(android.R.color.background_dark));
legend.setBackgroundResource(R.drawable.border_dark);
} else {
// same as above but for Holo_light
}
// And from here proceed to add the TimeCharts with using
renderer.setDisplayChartValues(false);
On my action bar I included buttons for Pause/Resume and ResetZoom.
the Reset Zoom is simple:
mRenderer.setXAxisMax(-Double.MAX_VALUE);
mRenderer.setXAxisMin(Double.MAX_VALUE);
mRenderer.setYAxisMax(-Double.MAX_VALUE);
mRenderer.setYAxisMin(Double.MAX_VALUE);
mChartView.repaint();
There's a background service that holds references to the TimeSeries that is always reading fresh data from the WiFi and adding them to the series. That way, in the fragment there's a runnable executing every 700ms that calls repaint() as:
// Chart rendering control ========================
private Runnable updateDataSet = new Runnable() {
public void run() {
if (!chartPaused) {
double max = mRenderer.getXAxisMax(); // get renderer maximum value
double min = mRenderer.getXAxisMin(); // get renderer minimum value
// Check if the user scrolled/zoomed/panned the graph or if it's on 'auto'
if (!((max == Double.MAX_VALUE || max == -Double.MAX_VALUE) && (min == Double.MAX_VALUE || min == -Double.MAX_VALUE))) {
double newMax = mDataset.getSeriesAt(0).getMaxX(); // new max is the latest value from our series
double newMin = newMax - Math.abs(max - min); // new min value is the X range the user zoomed into
mRenderer.setXAxisMax(newMax); // set the new values
mRenderer.setXAxisMin(newMin);
}
// Logic that updates the TableLayout with the legend is placed here
mChartView.repaint();
}
getView().postDelayed(updateDataSet, 700);
}
};
and there's a ZoomListener and a PanListener that pauses the chart every time the user touches it. That way the user can zoom and pan around, but whenever it resumes the chart updating it will only scroll forward to the earliest data without changing the zoom level.
I guess the final result is pretty solid and looks nice on both Holo_Light and Holo_Dark.
Thanks a LOT for Dan for the answers and for all other devs that created the engine.
happy coding!
Question 1. You can try to play with these API calls:
mRenderer.setShowLegend(false);
or
mRenderer.setMargins(new int[] {top, left, bottom, right});
Maybe decrease the bottom value?
Question 2.
a)
mRenderer.setXLabelsColor();
mRenderer.setYLabelsColor();
b)
mRenderer.setXLabelsAlign();
c)
I think you can do the following, when the series get new values:
mRenderer.setXAxisMin();
mRenderer.setXAxisMax();
Provide the x min and x max values as the range to be displayed and call mChartView.repaint(); after that.
d) Same as for c)
As a suggestion, please try to split such long questions in several smaller ones and you may get answers faster. There aren't many ACE users to know how to answer all questions. Even the author of this library has to think about some answers :)
For removal of scrolling of that chart : mRenderer.setZoomEnabled(false); mRenderer.setPanEnabled(false);
I'm new to GL and wanted to create a tiled map as a self tuorial. I want to create a small (maybe 7 hexes wide / tall) hex map. My first thought was to just create a method to draw one hex and then just translate the appropriate offset and place the new hex. But this doesn't seem effcient. Any Idea's? Alos as a side question, how do I determine if a MotionEvent is with in the are of a given hex?
Extensive hex grid information.
To determine if a MotionEvent is within a certain hex you have to convert the coords passed in via the motion event to your OpenGL World coords. Its just like a unit conversion, you know the screen goes from 0 - WIDTH and your GL world lets say goes from -1 to 1.
(xCoord / (Width - 0)) * (1 - (-1)) = xCoordWorld
will give you the xCoord from 0 to 2, then subtract 1 to get it in -1 to 1.
As far as the hexes go I've always used 'art' hexes. Draw the hex out in paint then render a bunch of squares with that piece of art on them, fast and easy to swap a hex out for another hex.
Google maps api v3 allows "styles" to be applied to the map, including setting the color of various features. However, the color format it uses is HSL (or what seems like it):
hue (an RGB hex string)
lightness (a floating point value between -100 and 100)
saturation (a floating point value between -100 and 100)
(from the docs)
I managed to find RGB to HSL converters online, but I am unsure how to specify the converted values in a way that google maps will accept. For instance, a typical HSL value given by a converter would be: 209° 72% 49%
How does that HSL value map to the parameters I specified from the google maps api? i.e. how does a hue degree value map to an RGB hex string and how does a percentage map to a floating point value between -100 and 100?
I am still uncertain how to do the conversion. I need to, given an RGB value, quickly convert it to what google maps expects so that the color will be identical...
Since the hue argument expects RGB, you can use the original color as the hue.
rgb2hsl.py:
#!/usr/bin/env python
def rgb2hsl(r, g, b):
#Hue: the RGB string
H = (r<<16) + (g<<8) + b
H = "0x%06X" % H
#convert to [0 - 1] range
r = float(r) / 0xFF
g = float(g) / 0xFF
b = float(b) / 0xFF
#http://en.wikipedia.org/wiki/HSL_and_HSV#Lightness
M = max(r,g,b)
m = min(r,g,b)
C = M - m
#Lightness
L = (M + m) / 2
#Saturation (HSL)
if L == 0:
S = 0
elif L <= .5:
S = C/(2*L)
else:
S = C/(2 - 2*L)
#gmaps wants values from -100 to 100
S = int(round(S * 200 - 100))
L = int(round(L * 200 - 100))
return (H, S, L)
def main(r, g, b):
r = int(r, base=16)
g = int(g, base=16)
b = int(b, base=16)
print rgb2hsl(r,g,b)
if __name__ == '__main__':
from sys import argv
main(*argv[1:])
Example:
$ ./rgb2hsl.py F0 FF FF
('0xF0FFFF', 100, 94)
Result:
Below is a screenshot showing the body set to a rgb background color (#2800E2 in this case), and a google map with styled road-geometry, using the values calculated as above ('0x2800E2', 100, -11).
It's pretty clear that google uses your styling to create around six different colors centered on the given color, with the outlines being closest to the input. I believe this is as close as it gets.
From experimentation with: http://gmaps-samples-v3.googlecode.com/svn/trunk/styledmaps/wizard/index.html
For water, gmaps subtracts a gamma of .5. To get the exact color you want, use the calculations above, and add that .5 gamma back.
like:
{
featureType: "water",
elementType: "geometry",
stylers: [
{ hue: "#2800e2" },
{ saturation: 100 },
{ lightness: -11 },
{ gamma: 0.5 },
]
}
We coded a tool which exactly does want you want. It takes hexadecimal RGB values and generates the needed HSL code. It comes with a preview and Google Map JavaScript API V3 code output. Enjoy ;D
http://googlemapscolorizr.stadtwerk.org/
From the linked page:
Note: while hue takes an HTML hex color value, it only uses this value to determine the basic color (its orientation around the color wheel), not its saturation or lightness, which are indicated separately as percentage changes. For example, the hue for pure green may be defined as "#00ff00" or "#000100" within the hue property and both hues will be identical. (Both values point to pure green in the HSL color model.) RGB hue values which consist of equal parts Red, Green and Blue — such as "#000000" (black) and "#FFFFFF" (white) and all the pure shades of grey — do not indicate a hue whatsoever, as none of those values indicate an orientation in the HSL coordinate space. To indicate black, white or grey, you must remove all saturation (set the value to -100) and adjust lightness instead.
At least as I read it, that means you need to convert your angle based on a color wheel. For example, let's assume 0 degrees is pure red, 120 degrees is pure blue and 240 degrees is pure green. You'd then take your angle, figure out which two primaries it falls between, and interpolate to determine how much of each primary to use. In theory you should probably use a quadratic interpolation -- but chances are that you can get by reasonably well with linear.
Using that, 90 degrees (for example) is 90/120 = 3/4ths of the way from red to blue, so your hex number for the hue would be 0x00010003 -- or any other number that had green set to 0, and a 1:3 ratio between red and blue.
I needed to match colors exactly. So I used the tool that #stadt.werk offers (http://googlemapscolorizr.stadtwerk.org/) to get close.
But then I ran into the problem explained by #bukzor where the Google Maps API creates variations on your shade, none of which seem to be exactly what I specified.
So I pulled up the map in a browser, took a screenshot of just the area with the two shades that weren't quite matching, opened it up in an image editor (pixlr.com, in my case), used the color-sucker tool to get the saturation and lightness for the shade, adjusted my saturation and/or lightness in the Google API call by 1, and repeated until I got something that seems to match perfectly.
It is possible, of course, that Google Maps API will do different things with the colors on different devices/browsers/etc., but so far, so good.
Tedious, yes, but it works.