INSPIRATION

The goal of this project is to create a process by which we can understand urban heat islands—the phenomenon wherein built and natural conditions conspire to raise temperature in urban areas during periods of hot weather—with readily accessibly data from satellite images. During a spell of hot weather in late July 2019, mercury levels in Philadelphia remained above 90 degrees Fahrenheit during the day and even held above 80 over night. Yet this swelter was not felt equally across the city: the hottest and coolest—comparably—portions of the city differed by as much as 20 degrees. A park could be bearable; a parking lot could be insufferable. Each neighborhood sees unique effects from sun and heat and each season is different. Because of this, the following project is an application that can be used by all. While this application represents a high cost to build, it yields a low cost for experimentation. The subsequent exploration is modest, but folding new cities and layering new methods only becomes easier with such an interface. Additionally, because it lacks advanced statistical analysis, forgoing the statistical for the graphical, it serves as a means to travel the globe by satellite—more qualitatively than quantitatively and with breadth rather than depth, although by allowing for such travel is by no means trivial. To explore the geography of heat with this tool, use this link.

Temperature in the United States

Mapping surface temperature throughout the year in the united states reveals that at times urban areas—like the Acela Corridor—are warmer than regions at comparable latitudes (see: code below to reproduce).

Remote sensing technology provides robust spatio-temporal data (USGS’s Landsat 8 satellite orbits the earth every 100 minutes and returns to any given location roughly once every two weeks), which, clouds permitting, can be converted into surface temperature. Yet urban heat islands exist at the intersection of global forces and local conditions—where the weather meets the road, so to speak. In addition to investigating the nature of heat islands, this project will also create a workflow and tools for exploring heat islands in counties across the United States. This application will allow any user to zoom in on an area, see hot spots within it, and generate facts and figures that shed additional light on the phenomenon and its possible causes locally.

Toward this end, the following uses Google Earth Engine, a platform to geospatial analysis using JavaScript. Because this is an exercise as well as application, in the interest of practice, it involves manifold sources and types of data. The first half will leverage Landsat data from the United States, the second Sentinel data from the European Union. While former has a spatial resolution of 30 meters squared, the latter is sharper at 15. The map above uses NASA’s Moderate Resolution Imaging Spectroradiometer (MODIS), which is valuable for its temporal if not its spatial resolution, imaging every point on earth every couple of days rather than weeks.

A note on compromises

Landsat and Sentinel offer distinct benefits. The former includes a thermal band, making it easy to calculate surface temperature; the latter does not. Yet Sentinel-2 provides a sharper resolution than Landsat 8, at 10 meters for most bands compared to 30. Further complicating the matter is the fact that Sentinel has not been live as long, making it difficult to measure change. Because this study is concerned with temperature, the ability to accurately derive it is important—to say the least. To be a valuable planning tool, we need a resolution that allows us to target aspects of the built environment driving heat islands. While early iterations involved pairing Sentinel images with temperature from Landsat, many of the covariates varied widely from the Landsat temperature—giving strikingly different contours—as visitation times differed. The visual benefit of seeing the features that define heat islands is compromised then. Staying consistent with Landsat data, though not as sharp, presents a clearer relationship between, say, temperature and vegetation, because points can be compared directly in time.

To create a layer of satellite imagery to ground our analysis of heat islands, we compensate for this using a technique called pan-sharpening, which combines a high resolution panchromatic band with the low resolution multispectral ones. This requires the Landsat 8 top-of-atmosphere, as opposed to surface, image collection; surface Landsat products do not have the panchromatic band (B8) necessary to polish the image.

var cloudy = ee.ImageCollection("LANDSAT/LC08/C01/T1_TOA")
                  .filterDate('2010-01-01', '2018-12-31')

We then filter out the cloudy images and run through a script from the guide, converting the collection into an image with the mosaic command so that it the function works as written. Note also that we end by embedding visualization parameters into the image with the visualize function, which is important to the functioning of the application.

var blurry   = cloudy.filter(ee.Filter.lt('CLOUD_COVER', 1));
var cleanest = blurry.mosaic()

var hsv = cleanest.select(['B4', 'B3', 'B2']).rgbToHsv();

var sharpest = ee.Image.cat([
  hsv.select('hue'), hsv.select('saturation'), cleanest.select('B8')
  ])
  .hsvToRgb()
  .visualize({min: 0, max: 0.25, gamma: [1.3, 1.3, 1.3]});

The result is a picture that is not perfect but markedly sharper than the raw image. This image will become a comparison when we layer temperature data over it, allowing us to see the urban forms that dispose an area to relative heat.

Sharpening

The value of sharpening satellite imagery, as above, comes from this improved clarity, as many blurred masses become defined buildings and the relationship between features of built environment become clear (see: code below to reproduce).

Aesthetic considerations

As open source software often does, Earth Engine has a strong community behind it. This project uses one particular contribution—a collection of palettes—from Gennadii Donchyts, Fedor Baart & Justin Braaten to improve its look. For more information and to see what other palettes are on offer, see this repository.

To pull in a palette without needing to code each hexadecimal string manually, load the package with the require function, then choose a palette. The example below shows the scheme used to produce the above animation. Although it saves time in scripting different palettes on the front end, calling the package does slow down processing on the back end.

var palettes = require('users/gena/packages:palettes');
var palette  = palettes.misc.kovesi.rainbow_bgyrm_35_85_c71[7];

Building up from there

With this map as a base, we can then layer on surface temperature along with a battery of explanatory variables—various indices to describe the landscape. This requires surface reflectance data, which we then mask using a function from the repository that holds them; much of this analysis uses 2018 but that year was marked by intense rainy and cloudy periods, so for longitudinal study we use 2016.

Toggling

The goal of this application is to allow users to toggle between an island of land surface temperature and any covariates of interest; this particularly interface facilitates quick transitions between layers to help the process

Rather than rely on the imagination for palettes, a few existing ones correspond roughly to the variables that we are looking at: green for the natural environment, grey for the built environment, and blue for water.

var dataset = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR')
                  .filterDate('2018-01-01', '2018-12-31')
                  .map(maskL8sr)

var palettes = require('users/gena/packages:palettes');

var built = palettes.kovesi.diverging_linear_bjy_30_90_c45[7];
var green = palettes.kovesi.linear_green_5_95_c69[7];
var water = palettes.kovesi.linear_blue_5_95_c73[7];                  

We will use three measures of the landscape as covariates: normalized difference vegetation index and modifications of this that correspond to buildings and bodes of water. NDVI is simply the (near infrared band - red band) / (near infrared band + red band), while NDWI is the (green band - near infrared band) / (green band - near infrared band) and NDBI is the (shortwave infrared band - near infrared band) / (shortwave infrared band + near infrared band). For more information, see this paper by Xiaoping Liu and colleagues. Earth Engine, however, has a function called normalizedDifference which takes care of that pesky division—the normalization. We save these equations as functions so that they can be mapped over collections.

var addNDBI = function(image) {
  var ndbi = image.normalizedDifference(['B6', 'B5']).rename('NDBI');
  return image.addBands(ndbi);
};

var addNDVI = function(image) {
  var ndvi = image.normalizedDifference(['B5', 'B4']).rename('NDVI');
  return image.addBands(ndvi);
};

var addNDWI = function(image) {
  var ndwi = image.normalizedDifference(['B3', 'B5']).rename('NDWI');
  return image.addBands(ndwi);
};

This section simply maps over the image collection with the indexing functions before creating a qualityMosaic. This flattens the collection into an image based on one variable, choosing the pixels that have the highest value across the stack—a mosaic of this sort is one of extremes.

var ndbi    = dataset.map(addNDBI);
var ndbiest = ndbi.qualityMosaic('NDBI').select('NDBI').visualize({min: 0, max: 1, palette: built});

var ndvi    = dataset.map(addNDVI);
var ndviest = ndvi.qualityMosaic('NDVI').select('NDVI').visualize({min: -1, max: 1, palette: green});

var ndwi    = dataset.map(addNDWI);
var ndwiest = ndwi.qualityMosaic('NDWI').select('NDWI').visualize({min: -1, max: 1, palette: water});

In order to convert this satellite imagery to temperature, we simply need to take the processed data and multiply it by the scaling variable—a constant that is available in the Earth Engine catalogue—before converting from Kelvin to Celsius. This function is then mapped over the image collection as in all other examples.

var addHeat = function(image){
  var heat = image.select(['B10']).multiply(0.1).subtract(273.5).rename('HEAT');
  return image.addBands(heat);
};

var temp    = dataset.map(addHeat);

var heat = palettes.kovesi.diverging_rainbow_bgymr_45_85_c67[7];
var hottest = temp.qualityMosaic('HEAT').select('HEAT').visualize({min: 18, max: 45, palette: heat});

To create a menu of options, we simply need to aggregate all of these layers into an object.

var images = {
  'temperature': hottest,
  'reality': sharpest,
  'natural': ndviest,
  'built': ndbiest,
  'water': ndwiest,
};

From here, we create two variables, one for the left side and the other for the right, along with two selectors—the buttons that will allow us to change the layer. Now that each selector is assigned to one side or the other, which is why they disappear if the user drags the curtain over them.

var leftMap = ui.Map();
leftMap.setControlVisibility(false);
var leftSelector = addLayerSelector(leftMap, 0, 'top-left');

var rightMap = ui.Map();
rightMap.setControlVisibility(false);
var rightSelector = addLayerSelector(rightMap, 1, 'top-right');

This function, wrapping another function, is what allows the user to change the layer; for an explanation of each line, see this example from the Earth Engine community. We add a selection widget that uses a menu of items—keys from the saved layers—and updates the map when that widget changes.

function addLayerSelector(mapToChange, defaultValue, position) {
  var label = ui.Label('Choose an image to visualize');

  function updateMap(selection) {
    mapToChange.layers().set(0, ui.Map.Layer(images[selection]));
  }

  var select = ui.Select({items: Object.keys(images), onChange: updateMap});
  select.setValue(Object.keys(images)[defaultValue], true);

  var controlPanel =
      ui.Panel({widgets: [label, select], style: {position: position}});

  mapToChange.add(controlPanel);
}

Finally, we set these two panels up as a splitPanel with wipe set to true, which gives the curtain effect.

var splitPanel = ui.SplitPanel({
  firstPanel: leftMap,
  secondPanel: rightMap,
  wipe: true,
  style: {stretch: 'both'}
});

ui.root.widgets().reset([splitPanel]);
var linker = ui.Map.Linker([leftMap, rightMap]);

leftMap.setCenter(-75.16037464340661, 39.95143720941659, 12);

Finally, we launch the application in the applications menu of the code editor. The result shows a search bar that allows us to move from city to city with ease.

Exploring

With the search bar, any planner or designer may select his or her city to conduct a more local analysis; this feature also encourages global analysis and comparison between cities. Note also that the the mosaic still shows seems from where we have combined images taken on different days, illustrating an issue with validity in the data.

EXPLORATION

We can augment this further with functions that allow for a spatio-temporal exploration. As a separate product, for simpler use, it is available at this link, but we will link it into the main application in the final product, able to be viewed below and used here.

To aid precision and to signal that this map accepts user input, we begin by changing the cursor style.

Map.style().set('cursor', 'crosshair');

From there, we set up and panel with its height and location before adding to the map.

var panel = ui.Panel();
panel.style().set({
  width: '400px',
  position: 'bottom-right'
});
Map.add(panel);

var intro = ui.Panel([
  ui.Label({
    value: 'Click to explore',
    style: {fontSize: '14px', fontWeight: 'bold'}
  })
]);

panel.add(intro)

The most advanced code comes from the reaction to a click. We need to create a variable—a feature collection—that stores both point clicked and a buffer around that point so that we have a comparison. We then instantiate a chart which uses a Reducer to calculate the mean temperature of the comparison (and the point but the mean for that is obviously its value).

Map.onClick(function(coords) {
  panel.clear();
  var point = ee.FeatureCollection([
    ee.Feature(  
      ee.Geometry.Point(coords.lon, coords.lat).buffer(10000), {label: 'Area Average'}),
    ee.Feature(
      ee.Geometry.Point(coords.lon, coords.lat), {label: 'Selected Zone'})
    ]);
  var chart = ui.Chart.image.seriesByRegion(
    temp, point, ee.Reducer.mean(), 'HEAT', 200, 'system:time_start', 'label')
        .setChartType('LineChart')
        .setOptions({
          title: 'Temperature over time',
          vAxis: {title: 'Temperature (Celcius)'},
          series: {
            0: {color: 'FD92FA'},
            1: {color: '0035F9'}

          }});
  panel.add(chart);
});

After we add this chart to the panel, we have an interface that responds well and its intuitive to use.

Testing

In order to assess the heat of a point in relation to the larger region, this application allows the user to click on that area of interest, returning the trend line from that point and a 10 kilometer buffer. We can export the chart using the button in the top right corner.

This product allows any individual—be they a resident, the head of a community group, a planner or otherwise a member city government—to make comparisons between one point and the larger region. The buffer encompasses many small cities and constitutes a significant portion of big ones, indicating that it is a meaningful unit of analysis.

Probing

While intended as a demonstration of how easy it is find and export patterns using this application, the above images reveal an interesting trend: the line hugging the average is Center City while the others are North and South Philadelphia, meaning the part represents the whole for one neighborhood but not for the other two, which are consistently hotter.

Indices

Beginning to understand the contours of heat in a city requires measures of the built and natural environments that influence them. A popular technique is normalized difference, which takes the value of two bands and—of course—subtracts one from the before dividing by the total value of both. Using this simple technique can shed light on how lush, how built, or how wet a region is—NDVI (vegetation), NDBI (built, or built-up), and NDWI respectively. Compared to NDVI, the enhanced vegetation index (EVI) fails to meaningfully portray the character of the city and it thus falls out of the analysis. We can view these indices simultaneously by modifying an existing script to create a similar one that uses our measures as layers, available here.

Various indices

Splitting maps across panels, as shown here, allows us to see relationships at once, rather toggling back and forth as earlier. We can see that there is no clear relationship between the built environment, as such, and temperature; rather vegetation and water explain and built character exists as a negative.

Using this tool

We can begin by looking at Philadelphia, which suffers from extreme during summer months, notably in July this year when in two separate weeks average surface temperature surpassed 90 degrees Fahrenheit. Generally, the poorest areas in Philadelphia are the hottest. A corridor of marked with hot spots stretches along Roosevelt Boulevard, West and North Philadelphia also absorb heat. While the peripheral neighborhoods can either be above or below average, central Philadelphia is indeed a microcosm of the city, tracking along the average.

Temperature in Philadelphia

The above animation shows average surface temperature in Philadelphia for each summer month over the past 10 years, beginning in May and ending in September. We can see that it begins cooler but even early on warmer spots emerge. The brightest spots show a temperature greater than 40 degree Celsius, or 105 degrees Fahrenheit; the darkest ones are lower than 18 degrees Celsius or 65 degrees Fahrenheit (see: code below to reproduce).

One feature of note is that most business districts and the glass edifices that define them do much better than areas for industry or freight. This may be because reflective buildings repel heat or because tall buildings do let heat reach the ground. This holds for New York and Philadelphia—though not San Francisco, which has long slung buildings just off the ocean and high rise ones near the bay, likely influencing temperatures.

In order to look deeper at the divides that define Philadelphia, we can load several neighborhoods into the same structure to see what differences emerge.

var regions = ee.FeatureCollection([
  ee.Feature(
    ee.Geometry.Point([-75.23754495468296, 39.95336436115298]), {label: 'Cobbs Creek'}),
  ee.Feature(
    ee.Geometry.Point([-75.2124823936478, 39.990462677224336]), {label: 'Fairmount Park'}),
  ee.Feature(
    ee.Geometry.Point([-75.09918588485874, 39.99335602534718]), {label: 'Kensington'}),
  ee.Feature(
    ee.Geometry.Point([-75.15855360271166, 39.954709566125096]), {label: 'Convention Center'}),
  ee.Feature(
    ee.Geometry.Point([-75.17194121026927, 39.94942350738968]), {label: 'Rittenhouse Square'})
    ]);

We add these points into the same chart as earlier, with the regions object rather than the points object that we constructed from user input. We can also use shapes, but to simplify the analysis, the following considers these five points. Earlier experimentation, though, showed that using larger regions causes the means to converge.

Zones over time

Vegetation over time

These charts show NDVI and LST, with the two largely moving together; gaps exist where cloud cover limited measurement, but patterns emerge. Note that although vegetation and temperature rise and fall together, the areas with the lushest environments are those the coolest temperatures at any given time during the year.

Because the interface facilitates quick-and-dirty analysis, we can use it get a feel for general patterns. One critical issue evident in the data is the value of parks. The ravines in Toronto appear as cooling ducts, the eponymous mount in Montreal a cooling tower. Central Park and Prospect Park appear in sharper relief in the heated paved landscape of New York, while Philadelphia’s four founding squares each stand out on the grid. As a matter of equity then, if neighborhood change a fraught task, perhaps ensuring that all residents have access to parks could mitigate the summer heat.

INVESTIGATION

We can also test a clear hypothesis with this interface. New York City is defined by distinct built typologies, with towers in the park, houses on a block, low mixed use, high residential and higher commercial, and much more. The website terrapattern allows any user to pick a square of urbanism in a select assemblage of cities and see similar forms and structures throughout the rest of that city. We can use this service to find points that correspond to features of natural and built environment, rather than selecting manually as the application facilitates. Does the morphology that defines neighborhoods like the Upper West Side and Upper East Side—which planners often attempt to replicate—offer more hospitable climes than tall commercial or residential?

Below we consider:

  1. Commercial towers
  2. Residential towers
  3. Mixed neighborhoods
  4. Parks
Patterns of New York

These images serves to ground analysis, showing that for the most part the algorithm found the appropriate typologies: parks are with other parks and so forth.

The ability to do this is built into the infrastructure constructed here: points grouped into geometries and comparisons across those geometries. We simply load the data from terrapattern after converting it from geojson to shape, using the asset manager in Earth Engine. Once we call features from the repository we need to transform them using the geometry command before binding them together. With this, the collection becomes a collection of point collections rather than a collection of multipoint features.

var regions = ee.FeatureCollection([
  ee.Feature(
    ee.FeatureCollection('users/asrenninger/terra_parks').geometry(), {label: 'Parks'}),
  ee.Feature(
    ee.FeatureCollection('users/asrenninger/terra_blocks').geometry(), {label: 'Blocks'}),
  ee.Feature(
    ee.FeatureCollection('users/asrenninger/terra_towers').geometry(), {label: 'Towers'}),
  ee.Feature(
    ee.FeatureCollection('users/asrenninger/terra_business').geometry(), {label: 'Business'})
    ]);

These data are then added into plot as above.

Guess and check

As shown in the charts below, the strength in this tool may help researchers test and adapt assumptions. Although in remote sensing green is typically much warmer than grey, we see strong differences in vegetation without comparable differences in temperature. Across the sample, there is no significant difference in temperature, according to t-tests with exported data, though there is one for vegetation. Perhaps this is because differences across the categories balance the distribution—there may indeed be warm parks and cool towers. The value in this tool is that it allows for any one to investigate the situation.

Typologies and heat

Typologies and chlorophyl

We see here that despite clear differences in vegetation, there is no consistent bias across built typologies in New York City: towers in the park appear just as hot as towers amongst other towers, and housing appears to be just as warm as recreative areas.

EXPECTATION

The final product, available here, provides a unified platform for professionals and curious individuals alike to explore the geography of heat in their city. Yet there is still more to be done.

The finished product

The result of the project is an application that doubtless makes discovering heat islands far easier, while presenting a menu of covariates to explore as a preliminary analysis.

The lion’s share of time on this project went to developing a graphical interface; this necessarily meant that many analytical components did not reach fruition. Yet several germs for future development cropped up: the project began as a global comparison of urban heat islands but the computational power required to divide the world into cities, compute the average heat in those cities on a given day, and then remove that average from the measurements on the ground—so as to compare a city with itself rather than all cities across the country or the globe—proved difficult. It was far easier to allow users to input a region and manage the calculations iteratively. Having said that, the concept succeed in contained geographies, like a single state or province. With improved parallel processing in future, the task may indeed be possible—if not with Landsat data than with MODIS.

To do this, we can start by building a new base, since with clipped environs we will see the context behind them.

var baseChange = [{featureType: 'all', stylers: [{saturation: -100}, {invert_lightness: true}]}];
Map.setOptions('baseChange', {'baseChange': baseChange});

From there, the process is relatively simple on its face. We need to load a parent geography from which we can compare children; in this case, we can use counties. I also experimented with United Nations Functional Urban Areas data along with derived urban boundaries from night lights or MODIS landcover. These work for single cities but not the globe. The code below uses features from the Earth Engine catalogue. We then add bands for temperature and water—which will be used as a mask to limit to computational demands of the task—before adding mosaics to the map for comparison. Note that the code below is structure to use names rather that codes, so that they can be determined by a user prompt.

var states = ee.FeatureCollection("TIGER/2016/States");
var counties = ee.FeatureCollection("TIGER/2016/Counties");

var filter = states.filter(ee.Filter.eq('NAME',"Pennsylvania"));
var county = counties.filterBounds(filter).filter(ee.Filter.eq('NAME',"Philadelphia"));

var dataset = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR')
                  .filterDate('2010-01-01', '2018-01-01')
                  .map(maskL8sr)
                  .filterBounds(counties)

var addHeat = function(image){
  var heat = image.select(['B10']).multiply(0.1).subtract(273.5).rename('HEAT');
  return image.addBands(heat);  
};

var addNDWI = function(image) {
  var ndwi = image.normalizedDifference(['B3', 'B5']).rename('NDWI');
  return image.addBands(ndwi);
};

var ndwi    = dataset.map(addNDWI);
var ndwiest = ndwi.qualityMosaic('NDWI').select('NDWI').clip(counties);

var temp    = dataset.map(addHeat)
var hottest = temp.qualityMosaic('HEAT').select('HEAT').clip(counties);

var palettes = require('users/gena/packages:palettes');
var heat = palettes.kovesi.rainbow_bgyrm_35_85_c71[7]

var land = ndwiest.select('NDWI').lte(0.5);
var hottest = hottest.mask(land);

Map.addLayer(counties)
Map.addLayer(hottest, {bands: ['HEAT'], min: 18, max: 45, palette: heat})

The computationally demanding portion comes when trying to reduce the resulting mosaic by region: too many regions uses too much bandwidth and frequently returns an error, though it does work for states like Connecticut and Rhode Island. The reduceRegions function accepts a collection of features and determines the aggregate value according to some function, in this case mean. We convert the summary back into an image which mirrors the dimensions of the last to we can perform calculations on the same cells across layers.


var means = hottest.reduceRegions({
  collection: counties,
  reducer: ee.Reducer.mean(),
  scale: 30,
});

var meanImage = means
  .filter(ee.Filter.notNull(['mean']))
  .reduceToImage({
    properties: ['mean'],
    reducer: ee.Reducer.first(),
});

var difference = hottest.subtract(meanImage)

Map.addLayer(meanImage, {min: 18, max: 50, palette: heat})
Map.addLayer(difference, {min: -10, max: 10, palette: heat})

Map.centerObject(counties)

Having added the resulting objects to the map we can see the results. This is best done through iteration rather than using large collections at once. The goal for this project was also to use these geographically relative conditions to create the Urban Areas Composite Index, which joins NDVI, NDBI and NDWI, as proposed by Liu et al.

Temperature against the average

This image depicts the raw temperature (left) and the temperature once we subtract the county average (right) for Philadelphia and Los Angeles, above and below respectively. We can see that Los Angeles, by virtue of its position on the globe, is much hotter, obscuring the patterns within the county, which is far warmer on the high ground and cooler on the low coast.

There is also the capability in Earth Engine to change the charts based on what is showing on the map, rather than strictly show NDVI and LST. Managing long stretches of code is difficult in the code editor but with more time, the entire application could be cleaned and modified to accommodate this use. Regardless, this suite of applications—and the unified interface above all—allow for planners and designers to quickly understand the conditions of a site, including its heat signature and any of the various covariates that could be influencing that signature.

EXPLANATION

The following code executes this project. None of it is my own, though some combinations—amalgamations of snippets and ideas—are from my mind. For an introduction to Earth Engine, see the guide for developers. Many snippets of code from this tutorial. The user interface is a chimera, emerging from many of the existing applications in this repository. Finally, many of the individual operations that underpin this analysis—notably, the transformation of terrapattern data, despite looking quite simple, required some tinkering—come from the notes and materials of Professor Dana Tomlin.

Behind the curtain


function maskL8sr(image) {
  var cloudShadowBitMask = (1 << 3);
  var cloudsBitMask = (1 << 5);
  var qa = image.select('pixel_qa');
  var mask = qa.bitwiseAnd(cloudShadowBitMask).eq(0)
                 .and(qa.bitwiseAnd(cloudsBitMask).eq(0));
  return image.updateMask(mask);
}

var states = ee.FeatureCollection("TIGER/2016/States");
var counties = ee.FeatureCollection("TIGER/2016/Counties");

var filter = states.filter(ee.Filter.eq('NAME',"Pennsylvania"));
var county = counties.filterBounds(filter).filter(ee.Filter.eq('NAME',"Philadelphia"));

var dataset = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR')
                  .filterDate('2016-01-01', '2016-12-31')
                  .map(maskL8sr)

var palettes = require('users/gena/packages:palettes');

var built = palettes.kovesi.diverging_linear_bjy_30_90_c45[7];
var green = palettes.kovesi.linear_green_5_95_c69[7];
var water = palettes.kovesi.linear_blue_5_95_c73[7];

var addNDBI = function(image) {
  var ndbi = image.normalizedDifference(['B6', 'B5']).rename('NDBI');
  return image.addBands(ndbi);
};

var addNDVI = function(image) {
  var ndvi = image.normalizedDifference(['B5', 'B4']).rename('NDVI');
  return image.addBands(ndvi);
};

var addNDWI = function(image) {
  var ndwi = image.normalizedDifference(['B3', 'B5']).rename('NDWI');
  return image.addBands(ndwi);
};

var ndbi    = dataset.map(addNDBI);
var ndbiest = ndbi.qualityMosaic('NDBI').select('NDBI').visualize({min: 0, max: 1, palette: built});

var ndvi    = dataset.map(addNDVI);
var ndviest = ndvi.qualityMosaic('NDVI').select('NDVI').visualize({min: -1, max: 1, palette: green});

var ndwi    = dataset.map(addNDWI);
var ndwiest = ndwi.qualityMosaic('NDWI').select('NDWI').visualize({min: -1, max: 1, palette: water});

var cloudy = ee.ImageCollection("LANDSAT/LC08/C01/T1_TOA")
                  .filterDate('2010-01-01', '2018-12-31')

var blurry   = cloudy.filter(ee.Filter.lt('CLOUD_COVER', 1));
var cleanest = blurry.mosaic()

var hsv = cleanest.select(['B4', 'B3', 'B2']).rgbToHsv();

var sharpest = ee.Image.cat([
  hsv.select('hue'), hsv.select('saturation'), cleanest.select('B8')
  ])
  .hsvToRgb()
  .visualize({min: 0, max: 0.25, gamma: [1.3, 1.3, 1.3]});

var addHeat = function(image){
  var heat = image.select(['B10']).multiply(0.1).subtract(273.5).rename('HEAT');
  return image.addBands(heat);

}

var temp    = dataset.map(addHeat)

var heat = palettes.kovesi.diverging_rainbow_bgymr_45_85_c67[7]

var hottest = temp.qualityMosaic('HEAT').select('HEAT').visualize({min: 18, max: 45, palette: heat});

The interface


var images = {
  'temperature': hottest,
  'reality': sharpest,
  'natural': ndviest,
  'built': ndbiest,
  'water': ndwiest,
};

var leftMap = ui.Map();
leftMap.setControlVisibility(false);
var leftSelector = addLayerSelector(leftMap, 0, 'top-left');

var leftPanel = ui.Panel();
leftPanel.style().set({
  width: '400px',
  position: 'bottom-right'
});

var leftIntro = ui.Panel([
  ui.Label({
    value: 'Click to explore',
    style: {fontSize: '14px', fontWeight: 'bold'}
  })
]);

leftMap.add(leftPanel);
leftPanel.add(leftIntro)

leftMap.style().set('cursor', 'crosshair');

leftMap.onClick(function(coords) {
  leftPanel.clear();
  var point = ee.FeatureCollection([
    ee.Feature(  
      ee.Geometry.Point(coords.lon, coords.lat).buffer(10000), {label: 'Area Average'}),
    ee.Feature(  
      ee.Geometry.Point(coords.lon, coords.lat), {label: 'Selected Zone'})
    ]);
  var chart = ui.Chart.image.seriesByRegion(
    temp, point, ee.Reducer.mean(), 'HEAT', 200, 'system:time_start', 'label')
        .setChartType('LineChart')
        .setOptions({
          title: 'Temperature over time',
          vAxis: {title: 'Temperature (Celcius)'},
          series: {
            0: {color: 'FD92FA'},
            1: {color: '0035F9'}

          }});
  leftPanel.add(chart);
});

var rightMap = ui.Map();
rightMap.setControlVisibility(false);
var rightSelector = addLayerSelector(rightMap, 1, 'top-right');

var rightPanel = ui.Panel();
rightPanel.style().set({
  width: '400px',
  position: 'bottom-left'
});

var rightIntro = ui.Panel([
  ui.Label({
    value: 'Click to explore',
    style: {fontSize: '14px', fontWeight: 'bold'}
  })
]);

rightMap.add(rightPanel);
rightPanel.add(rightIntro)

leftMap.onClick(function(coords) {
  rightPanel.clear();
  var point = ee.FeatureCollection([
    ee.Feature(  
      ee.Geometry.Point(coords.lon, coords.lat).buffer(10000), {label: 'Area Average'}),
    ee.Feature(  
      ee.Geometry.Point(coords.lon, coords.lat), {label: 'Selected Zone'})
    ]);
  var chart = ui.Chart.image.seriesByRegion(
    ndvi, point, ee.Reducer.mean(), 'NDVI', 200, 'system:time_start', 'label')
        .setChartType('LineChart')
        .setOptions({
          title: 'Vegetation over time',
          vAxis: {title: 'Vegetation (Normalized)'},
          series: {
            0: {color: '093805'},
            1: {color: '35E415'}

          }});
  rightPanel.add(chart);
});

function addLayerSelector(mapToChange, defaultValue, position) {
  var label = ui.Label('Choose an image to visualize');

  function updateMap(selection) {
    mapToChange.layers().set(0, ui.Map.Layer(images[selection]));
  }

  var select = ui.Select({items: Object.keys(images), onChange: updateMap});
  select.setValue(Object.keys(images)[defaultValue], true);

  var controlPanel =
      ui.Panel({widgets: [label, select], style: {position: position}});

  mapToChange.add(controlPanel);
}

var splitPanel = ui.SplitPanel({
  firstPanel: leftMap,
  secondPanel: rightMap,
  wipe: true,
  style: {stretch: 'both'}
});

ui.root.widgets().reset([splitPanel]);
var linker = ui.Map.Linker([leftMap, rightMap]);
leftMap.centerObject(county);

How to Animate Surface Temperature

For a thorough explanation of this process, please see this Justin Braaten tutorial. Here I largely recapitulate his explanation but with a few suggestions to modify it for the urban scale.

var collection = ee.ImageCollection("MODIS/006/MOD11A2").select('LST_Day_1km');

First we then set bounding box for the United States before transitioning into data processing.

var mask = ee.FeatureCollection("TIGER/2016/States");

var region = ee.Geometry.Polygon(
  [[[-127.76347656249999, 49.42862428430654],
          [-127.76347656249999, 23.46798096552757],
          [-65.09746093749999, 23.46798096552757],
          [-65.09746093749999, 49.42862428430654]]],
          null, false
);

This section uses the getRelative function to determine the day of the year; we then select one year and then join the days from all other years to that list of days. We finish by taking the median, but could just have easily chosen the mean. Note that in the animation of Philadelphia, we change clip to a county and change the day parameter to month. This is because Landsat returns less frequently than does MODIS and there is always the right of clouds; grouping by month helps contain the effect of cover on any given day.

var collection = collection.map(function(img) {
  var doy = ee.Date(img.get('system:time_start')).getRelative('day', 'year');
  return img.set('doy', doy);
});

var distinctDOY = col.filterDate('2013-01-01', '2014-01-01');

var filter = ee.Filter.equals({leftField: 'doy', rightField: 'doy'});

var join = ee.Join.saveAll('doy_matches');

var joinCol = ee.ImageCollection(join.apply(distinctDOY, collection, filter));

var comp = joinCol.map(function(img) {
  var doyCol = ee.ImageCollection.fromImages(
    img.get('doy_matches')
  );
  return doyCol.reduce(ee.Reducer.median());
});

In order to plot and animate this, we simply pull in a palette from the Earth Engine community, as above, set parameters and then print the it.

var palettes = require('users/gena/packages:palettes');
var palette = palettes.kovesi.rainbow_bgyrm_35_85_c71[7];

var visParams = {
  min: 14000.0,
  max: 16000.0,
  palette: palette,
};

var rgbVis = comp.map(function(img) {
  return img.visualize(visParams).clip(mask);
});

var gifParams = {
  'region': region,
  'dimensions': 600,
  'crs': 'EPSG:5071',
  'framesPerSecond': 10
};

print(ui.Thumbnail(rgbVis, gifParams));