tech explorers, welcome!

Author: TRW (Page 3 of 4)

Process satellite imagery from Landsat or Sentinel 2 with QGIS

If you know my #WarTraces post, you’ll see that I use satellite imagery from the European Space Agency. Today I’ll show how to download and manipulate these images from mission Sentinel 2, as well as Landsat, being very similar to do.

I’ll do it in QGIS, so you need to download this software with the GRASS GIS module included.

Sign up in platforms.

You need to create an account to access both data sources, just filling a form and confirming via email.

Copernicus Open Access Hub

https://scihub.copernicus.eu/dhus/#/self-registration

NASA Earthdata Search

https://urs.earthdata.nasa.gov/users/new

Download data.

Let's make a difference according to the access platform.

Copernicus Open Access Hub

Access the viewer and log in with the new credentials.

https://scihub.copernicus.eu/dhus/

Now, draw a delimiting polygon of the area of interest. Every corner is set with right click, and the polygon is closed with double right click.

Next, click the filter icon in the search bar and tick the box for mission Sentinel-2 (you can also set dates, but it will be sorted from recent to old by default).

Finally, all available products will appear, so you can preview or download in a compressed .zip file.

NASA Earthdata Search

Like before, log in on the viewer.

https://search.earthdata.nasa.gov/search

Define an area of interest using the geometry tools in the lateral bar.

Filter the products, for example selecting "Imagery". Then select the HLS Landsat dataset in the catalogue.

In this case, we need to download each band separately, which we'll see next.

TCI image (true colour)

Simplifying (a lot), satellites capture images in different wave longitude ranges, called bands.

Also, digital images or pictures that we use to represent reality seen by the naked eye use to be defined in three layers: one for color Red, another for color Green, and another one for Blue, defining in each of these layers one value for every pixel with the amount of this color. The combination of all pixels is what simulates an image similar to reality.

TIP! Do the test and zoom in a lot in any picture and see the mosaic of pixels that make it up.

Lets use the bands captured by satellites in the visible wave range to generate an image as if we saw it from space.

Sentinel 2

In the case of Sentinel 2, we can show a true color image directly downloading the product L1C and using the layer _TCI.jp2 located in /GRANULE/DATASET_CODE_NAME/IMG_DATA/

The TCI image will show like this:

Deir ez Zor, Syria, 22/10/2017. TCI. Source: Copernicus Sentinel 2.

Landsat

TIP! https://gisgeography.com/landsat-8-bands-combinations/

For Landsat, we need to combine the different bands captured individually by the satellite in the visible wave range. These are:

REDB04
GREENB03
BLUEB02

Now in QGIS, we'll use function i.group from GRASS module which combines the selected layers in a multiband Red, Green, Blue layer.

To make it easier, I rename the layers like this, to make sure the script reads them properly:

Looking like this:

In this case it's nothing similar to the previous image or reality, so we'll adjust the properties to extract more color from the data:

  • First, let's make sure we use all the data reading real max/min values rather than estimated.
  • The image already looks better, but missing some brightness and color.
  • So let's adjust Color Rendering parameters:

And it now looks more realistic:

Deir ez Zor, Siria, 21/10/2017. TCI. Source: Landsat.

SWIR (short wave infra-red)

Using bands in infra-red wave length, we can see further more than with the naked eye, or even across certain objects like some clouds or smoke.

During summer wildfires, I saw how Copernicus used these images to trace the extent of fires, and I thought they could be used to locate bomb impacts under the smoke they produce. That's how this ended in #WarTraces.

Sentinel 2

Using the process above, now we combine the following bands:

REDB12
GREENB8A
BLUEB04
Deir ez Zor, Syria, 22/10/2017. SWIR. Source: Copernicus Sentinel 2.

Landsat

Make a new combination of these bands:

REDB07
GREENB06
BLUEB04
Deir ez Zor, Syria, 21/10/2017. SWIR. Source: Landsat.

Conclusion

As a final note, it's important to highlight the pixel size, as it will affect image detail (resolution):

  • Sentinel 2: 10 meters.
  • Landsat: 30 meters.

On the other hand, Landsat provides images since 2013, while Sentinel only provides data from 2015.

Also, you can preview all these layers and combinations in online viewers like EO Browser by Sentinel Hub.

Bombings in Antonov International Airport, Kyiv, Ukraine, 26/2/2022 using EO Browser.

If you get stuck or want to comment, let me know on 🐦 Twitter!

🐦 @RoamingWorkshop

BlenderGIS: 3D modelling in Blender with geographical information

Blender is (to me), the top free 3D modelling software. It’s got a huge community, tones of documentation, tutorials and, overall, continuous updates and improvement.

One of the most useful tools is BlenderGIS, an external plugin that lets us drop geographical data, georeferenced or not, and model with them.

Let’s see a use case with an elevation model.

Installation

First thing to do is to download and install Blender from an official source (their website) or from our OS app store:

https://www.blender.org/download/

Now, download BlenderGIS from the author's github as a .zip file:

https://github.com/domlysz/BlenderGIS

Let's now start Blender and open Add-ons settings (Edit > Preferences > Add-ons).

Press "Install..." and select the BlenderGIS .zip file.

Now you can search it and activate it.

You'll see there's a new "GIS" option in Blender's top bar.

Download geographical information

In this example I will use a Digital Terrain Model in ASCII format (.asc), as it is one of the working formats for BlenderGIS and also the standard for my download source.

If the information you download is in another format, like .tiff or .xyz, you can convert it using some software like QGIS or ArcGIS.

MDT

In my case, I will use MDT200 from spanish IGN, a terrain model with 200m cell size, as I want to show quite a large area that includes the province of Álava.

https://centrodedescargas.cnig.es/CentroDescargas/index.jsp

Orthophoto

We can also use an orthophoto as a terrain texture. For this, I will be using QGIS and I will load a WMS also from IGN so I can clip the satellite image exactly to the terrain tile extent.

Load the orthophoto and the terrain layers, then export the orthophoto layer as a rendered image and set the extension from the terrain layer. I will set the cell size to 20 meters (although the imagery allows up to 20cm cell size, which would result in a huge file; the 20m image is already 140MB).

TIP! One way to optimize detail is to generate a grid of smaller size images but higher resolution.

Modelling in Blender

Now it's all ready for Blender.

Using the "GIS" menu, import the terrain layers as an ASC grid. You'll see it quickly shows up on screen.

TIP! This model is centered in coordinate origin, but you can georeference the model setting a CRS in the "Geoscene" properties.

Let's add the satellite image:

  1. Create a new material.
  1. Create a new texture and load the satellite image.
  1. Now move to UV Editing tab.
    • Select the terrain layer on the right window, enter Edit Mode, and "Select all" faces (Ctrl+A). You should see it orange as below and make sure you are in "Top" view (press number 7).
    • Click on "UV" tools in the top menu and project the terrain layer with "Project from View (bounds)". This makes it fit the image extent.
  1. On the left window, choose the image texture to apply to the projection and see how the grid adjusts to it (try making zoom on it)
  1. Finally, go to the Shading tab and add the element "Image Texture", choosing the right image and connecting Vector to UV and Color to Shader (just copy the image below).

If you now go to the Layout window, the model will show the satellite image perfectly adjusted.

And it's ready so you can edit and export your model, for example, for 3D printing or for a realistic Unity3D scene.

Doubts or comments? Come over 🐦 Twitter!

🐦 @RoamingWorkshop

Check tracking done by websites you visit.

If you want to check some of the tracking that websites do while you browse, make this simple experiment:

  1. Open the browser inspector with Ctrl+I or right click and “Inspect”
  2. Open the “Network” tab.
  3. Now refresh the main page of this blog.

This is what you’ll see: just 8 files that load all what’s needed to browse this blog.

Now open any other website that you use to visit and observe the content that’s loaded.

I’m going to visit amazon.es without any further interaction. Just see there’s a file at the end being created every couple of seconds /1/batch/1/OE . Now move the cursor without scrolling the window. The new files will show a variable t for the time you’ve been on the site, and coordinates x and y that show where you place the cursor. This is just an example of how tracking is used very often: where did you access the site from, what you click, what you see, where you stop, you IP, geolocation… There’s nothing of that in here.

This is only Amazon and its AWS. Google tracking shows up as its famous “analytics” or “collect” files in the Network tab as well.

It’s often said that the simplest is the best solution, in fact, you’ll see in this blog that you don’t need so many frills to publish a website. So don’t trust complicated sites, with never-ending “partners” lists and with Google Analytics or Amazon Web Services.

Take care of your browsing!

AnalogTouch: touch detection with any analog pin on Arduino

One of the ways to avoid roomy buttons in your Arduino projects is using boards or modules with tactile pins, like some ESP32 modules.

Despite this, the majority of the boards do not include this feature, but reading this entry in esp32.com forum y found something interesting:

https://www.esp32.com/viewtopic.php?t=23066

With a quick calibration you can use any analog pin as a tactile sensor. Despite there are a few libraries about this, you really just need a couple of function, so let’s get on with it!

Components

For this project I am using these simple components:

Wiring

If using the Pro Micro, this is the pin assignment:

Pro MicroComponente
Puerto Micro USBCable USB
Pin 21 (A3)Cable puente desconectado
Pin 20LED+
GNDResistencia

All you need to take into account is to connect the juper wire, that we will use as tactile sensor, into an analog pin.

(the board shown is not exactly a Pro Micro)

Arduino sketch

You'll find a ready to use Arduino sketch in my github:

https://github.com/TheRoam/TRW_AnalogTouch/blob/main/ProMicro_Blink_AnalogTouch.ino

It states "ProMicro" but it can really be used with any compatible microcontroller board. You just need to select your board in the board manager.

The Pro Micro runs a ATMega32u4, which is compatible with the Arduino Leonardo board.

With this config, compile the script and upload to the board.

Result

Open the Serial Monitor, which should show blank as the debugging text is set to minimum by default.

Now we touch the free pin of the jumper wire and see how the led lights, and how the "touch" is detected in the Serial Monitor.

You'll also see that the script accumulates "touches" so this can be used in other programs.

The code

Briefly this is what the program does:

  1. During setup(), start serial communication, activate led pin and fill-up the analog readings array with setTouch (Reads 20 values by default).
void setup() {
  // initialize serial communication at 115200 bits per second:
  Serial.begin(115200);
  //enable LED pin
  pinMode(LP,OUTPUT);
  setTouch(TP);
}
  1. Next we run the loop() function, where the readings are updated at the start of each loop.
setTouch(TP);
//------loop continues------//
void setTouch(int tp){
  //fill up the array
  for(int a=0;a<VALUES;a++){
    mean[a]=analogRead(tp);
    delay(10);
  }
}
  1. Check if debugging is minimal or extended, and run the main function (the only difference is the amount of debugging messages).
//run check
  if(out==1){
    extended(TP);
  }else if(out==0){
    minimal(TP);
  }
  1. Then, the main function:
    1. Runs through the reading array,
    2. Obtains the mean value,
    3. Gets a new reading,
    4. Calculates the difference between new reading and average,
    5. If the difference is bigger than the REFV voltage drop value: "touch" is identified, it's printed in Serial Monitor, touch counter is increased and led is lit.
    6. Restore average to 0, turn led off and return "touch" variable.
int minimal(int tp){
  //start reading values
  for(int i=0;i<VALUES;i++){
    //get average of array
    for(int j=0;j<VALUES;j++){
      avg+=mean[j];
    }
    
    avg=avg/VALUES;

    //read current value
    fin=analogRead(tp);
    
    //compare average with current value
    if((avg-fin)>REFV){
      Serial.println("----------  T   O    U    C    H  ----------");
      //increase counter
      touch++;
      //print touch count
      Serial.print("T: ");
      Serial.println(touch);
      //light led
      ledON(LP);
      //wait 1 second after touch detected
      delay(1000);
    }
    //restart counter
    avg=0;
    //restart led
    ledOFF(LP);
    delay(VALINTERVAL);
  }
  return touch;
}
  1. After this the loop is restarted, going back to point 2.

Other configurations

The script includes different configuration variables that we find under the '#define's:

  • OUT: defines the amount of debugging text. Value 0: minimal; Value 1: extended.
  • VALUES: analog readings array size.
  • TOUCHPIN: define the analog pin number used for touch.
  • LEDPIN: define the pin number used for led.
  • VALINTERVAL: evaluation interval in milliseconds.
  • REFV: voltage drop value (in millivolts) which identifies a "touch".
// OUTPUT verbosity
// 1: extended --> Prints ADC array, mean value, touch count and touch message with touch count
// 0: minimal  --> Prints touch message with touch count

#define OUT 0

// comparison variables
#define VALUES 20       //values for mean array
#define TOUCHPIN 3      //touch pin
#define LEDPIN 20       //led pin
#define VALINTERVAL 100 //evaluation interval
#define REFV 80         //reference voltage drop in mV

Calibration

Changing the configuration you can calibrate the script for different boards. From what I've tested, this is a good sequence:

  1. Set a high REFV, for example 100.
  2. Run the extended script with OUT 1
  3. Open Serial Monitor and have a look at the average values for a while (e.g.: 230)
  4. Now touch the jumper wire and see what the values are now (e.g.: 170)
  5. Make the difference between the average at 3. and the average at 4 (230-170=60).
  6. Set your REFV to this value (60) and play around it.
    • For more sensitivity, set it a bit lower (50)
    • If there is detection with no touch, increase it (80)
    • Because of CPU load, REFV value can vary if OUT 1 or OUT 0

Found it useful? Any doubts? Leave your comment on 🐦 Twitter!

🐦 @RoamingWorkshop

TurfJS: interactive geospatial tools for your web maps

If you’ve used desktop GIS software, you will be surprised that many geospatial tools are also available to use in your web maps using a free and open-source javascript library.

Today I show you TurfJS. Press the button 🌤 in the map below and see one of many possibilities: inverse distance weighting or IDW.

Installation

TurfJS hosts a practical website explaining all available functions, as well as the initial setup:

https://turfjs.org/getting-started

To use Turf you only need to link the library in your map using the CDN link:

<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>

Base map

You can create a basemap, for example, with Leaflet. TurfJS uses JSON sintaxis, so it's highly compatible with the geoJSON capabilities in Leaflet.

TIP! Leaflet has been recently updated to version 1.9.1. This post hasn't been updated and still uses version 1.7.1.

I will recycle this example from one of my first posts with a few changes:

  • Add the Turf library link in the <head>.
  • Define the markers in a data matrix or array, similar to how they are obtained from any data source in JSON format:

https://www.w3schools.com/js/js_arrays.asp

var markers=[
{"name":"Estacion1",
"lat":25.77,
"lon":-80.13,
"dato":26.4},
{"name":"Estacion2",
"lat":25.795,
"lon":-80.21,
"dato":29.8},
{"name":"Estacion3",
"lat":25.70,
"lon":-80.275,
"dato":31.35}
];

TIP! Numeric values won't go in quotation marks, otherwise Turf will take it as a String (something that doesn't happen in Leaflet).

  • Add the markers to the map in a loop, using forEach(), widely used in these cases:

https://www.w3schools.com/jsref/jsref_foreach.asp

markers.forEach(function(m){
    var marker=L.marker([m.lat, m.lon]).addTo(map);
    marker.bindTooltip("<b>"+m.name+"</b><br>"+m.dato).openTooltip();
});

//WE ADDED A SINGLE MARKER LIKE THIS ON LEAFLET
//var marker = L.marker([25.77, -80.13]).addTo(map);
//marker.bindTooltip("<b>Estacion1</b><br>m.dato").openTooltip();

Looking like this:

and this one being its code:

<head>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
  integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
  crossorigin=""/>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
  integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
  crossorigin=""></script>
<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>
</head>

<style>
#map{border-radius: 30px;}
</style>

<body>
<div id="map" style="height:500px;"></div>
</body>

<script>
var map = L.map('map').setView([25.75, -80.2], 12);
L.tileLayer("http://a.tile.openstreetmap.org/{z}/{x}/{y}.png", {/*no properties*/
}).addTo(map);

var markers=[
{"name":"Estacion1",
"lat":25.77,
"lon":-80.13,
"dato":26.4},
{"name":"Estacion2",
"lat":25.795,
"lon":-80.21,
"dato":29.8},
{"name":"Estacion3",
"lat":25.70,
"lon":-80.275,
"dato":31.35}
];

markers.forEach(function(m){
    var marker=L.marker([m.lat, m.lon]).addTo(map);
    marker.bindTooltip("<b>"+m.name+"</b><br>"+m.dato).openTooltip();
});
</script>

IDW interpolation with TurfJS

In the form above, data is now easily used by TurfJS.

One of the most interesting tools is the interpolation of data in a regular grid.

https://turfjs.org/docs/#interpolate

This is done using a very usual method: inverse distance weighting (IDW).

IDW from Wikipedia

Following the example given by TurfJS:

  1. Convert data into a Turf point layer, a featureCollection:

https://turfjs.org/docs/#featureCollection

//empty points array
var points=[];

//loop to fill the array with data
markers.forEach(function(m){
    points.push(turf.point([m.lon, m.lat],{name: m.name, dato: m.dato}));
});

//convert to Turf featureCollection
points=turf.featureCollection(points);

TIP! Note that Turf taked coordinates as longitude-latitude, instead of the usual latitude-longitude!

  1. Define interpolation options, having the following:
    • gridType: or cell shape
      • points
      • square
      • hex
      • triangle
    • property: field that contains the interpolation values.
    • units: spatial units used in the interpolation:
      • miles
      • kilometers
      • radians
      • degrees
    • weight: power used in the inverse distance weighting, usually 2, although higher values will make a smoother result.
//interpolation options: rectangular grid using 'dato' variable in kilometers using the power of 2

var options = {gridType: 'square', property: 'dato', units: 'kilometers', weight: 2};
  1. Finally, generate the interpolated grid and add it to the map, being the numeric value the cell size of the grid (0.5 Km):
//create Turf grid
var malla=turf.interpolate(points, 0.5, options);

//add to Leaflet
L.geoJSON(malla).addTo(map);

Generating this result (nothing conclusive):

Format grid and show values

Seems not, but the interpolation is done, it's just shown incorrectly.

I'll add a color ramp to represent data, and also show values when hovering the cells. Let's modify the Leaflet geoJSON object properties for this:

  • style: function to modify object style, according to its value.
  • onEachFeature: function added to each object to interact with it.

https://leafletjs.com/reference.html#geojson

https://leafletjs.com/examples/choropleth/

L.geoJSON(malla,{
  style:function(feature){
  var val=feature.properties.dato;
  if(val>30){
    return {fillColor: "orangered", fillOpacity: 0.5, weight:0};
  }else if(val>28 && val < 30){
    return {fillColor: "yellowgreen", fillOpacity: 0.5, weight:0};
  }else if(val>26 && val < 28){
    return {fillColor: "darkgreen", fillOpacity: 0.5, weight:0};
  }
},
onEachFeature:function(feature,layer){
  layer.on({
  mouseover:function(e){
    val=e.target.feature.properties.dato.toFixed(2);
    e.target.bindTooltip(val).openTooltip();
  }
  });
}
}).addTo(map);

Ending like this:

And here is the full code:

<head>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
  integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
  crossorigin=""/>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
  integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
  crossorigin=""></script>
<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>
</head>

<style>
#map{border-radius: 30px;}
</style>

<body>
<div id="map" style="height:500px;"></div>
</body>

<script>
var map = L.map('map').setView([25.75, -80.2], 12);
L.tileLayer("http://a.tile.openstreetmap.org/{z}/{x}/{y}.png", {/*no properties*/
}).addTo(map);

var markers=[
{"name":"Estacion1",
"lat":25.77,
"lon":-80.13,
"dato":26.4},
{"name":"Estacion2",
"lat":25.795,
"lon":-80.21,
"dato":29.8},
{"name":"Estacion3",
"lat":25.70,
"lon":-80.275,
"dato":31.35}
];

markers.forEach(function(m){
    var marker=L.marker([m.lat, m.lon]).addTo(map);
    marker.bindTooltip("<b>"+m.name+"</b><br>"+m.dato).openTooltip();
});

//empty points array
var points=[];

//loop to fill the array with data
markers.forEach(function(m){
    points.push(turf.point([m.lon, m.lat],{name: m.name, dato: m.dato}));
});

//convert to Turf featureCollection
points=turf.featureCollection(points);

//interpolation options: rectangular grid using 'dato' variable in kilometers using the power of 2

var options = {gridType: 'square', property: 'dato', units: 'kilometers', weight: 2};

//create Turf grid
var malla=turf.interpolate(points, 0.5, options);

malla=L.geoJSON(malla,{
  style:function(feature){
  var val=feature.properties.dato;
  if(val>30){
    return {fillColor: "orangered", fillOpacity: 0.5, weight:0};
  }else if(val>28 && val < 30){
    return {fillColor: "yellowgreen", fillOpacity: 0.5, weight:0};
  }else if(val>26 && val < 28){
    return {fillColor: "darkgreen", fillOpacity: 0.5, weight:0};
  }
},
onEachFeature:function(feature,layer){
  layer.on({
  mouseover:function(e){
    val=e.target.feature.properties.dato.toFixed(2);
    e.target.bindTooltip(val).openTooltip();
  }
  });
}
}).addTo(map);

</script>

Applications

Applying this to real data, I modified the leafMET library to add the interpolation using isobands.

It's the map you saw at the beginning which you can see fullscreen:

https://theroamingworkshop.cloud/leafMET/leafMET+Turf.html

The code is also available as a new branch of leafMET in github:

https://github.com/TheRoam/leafMET/tree/leafMET+Turf

And that's all! Hope you find it useful!

Any doubts or comments are awaited on 🐦 Twitter!

🐦 @RoamingWorkshop

endleZZ: a javascript infinite & random zombie suvival minigame

endleZZ is an infinite zombie survival minigame developed in javascript, making it available for any device with an internet browser.

Despite of this, the game can be played offline, running the .html file locally which you can download from github.

https://github.com/TheRoam/endleZZ

Version v0.1 is an initial proof-of-concept release with all the main features enabled for testing.

Otherwise, you can play the online release version which I host in my server:

https://theroamingworkshop.cloud/endlezz

TIP! Always download files from the official source published bh me, the author. DONT execute files shared by third parties. Javascript code can de easily manipulated to run malicious activities.

Description

You are a survivor with unlimited ammo surrounded by zombies, how long can you survive?

Randomness will make every game unique: zombie enemies are randomly generated in random border locations with random attributes (speed and level).

There are 4 zombie levels, making them bigger and harder to kill (1×1, 2×2, 3×3, 4×4).

The game is infinite as long as you can keep up killing zombies.

Wasting ammo reduces your score though, so make accurate shots!

Game instructions

  1. Click or touch a point in the map to shoot to that place.
  2. Every bullet used deduces 1 point (-1).
  3. Shooting body parts (light green) adds 1 point (+1).
  4. Shooting in the head (dark green) adds 10 points (+10).
  5. Survival time will also add score points in the future.

Current features

  • Multi-platform
  • Portable (single .html executable)
  • Lightweight: 20 KB
  • Offline: download and play locally.
  • Infinite survival game
  • Random enemy spawn time
  • Random enemy spawn location
  • Random enemy features
    • Speed
    • Level
  • Modular enemies (multi-part)
    • Body (light green)
    • Head (dark green)
  • Complex scoring system
    • (Hit) Body hit +1
    • (Kill) Head shot +10
    • (Bullet) Used bullet -1
    • (Time) Survival time [Not implemented]
  • Animations

Feature brainstorming

  • options menu
  • final score summary page
  • more complex graphics
  • day-night cycle
  • weather system: clouds, fog, rain, lightning…
  • map objects: trees, walls..
  • complex enemies
  • enemy loot: bullets, points.
  • powers: slow-down, kill-all, one-shot-kill…

Development

You can visit the development page in github:

https://github.com/TheRoam/endleZZ

If you have ideas, questions or comments, you can drop them on 🐦 Twitter!

🐦 @RoamingWorkshop

Using the open data API AEMET OpenData

In this post I explained how to directly add AEMET stations with certain data, using my leafMET plugin for Leaflet (available on github):

https://github.com/TheRoam/leafMET

Let’s now pay attention to the detail of the API and how to access all the available data.

API access

To use the API we need an API key. Go to the AEMET OpenData webpage and click on "Solicitar" to request it.

Next we'll be asked for an email address where the key will be sent.

You'll receive a first confirmation email, and then a second email with the access key. Let's copy it and keep moving.

Documentation and examples

Get back to AEMET OpenData and into the developer's section "Acceso Desarrolladores".

We're interested in the dynamic documentation, which shows all the data requests available that we can try live.

Click on each topic to display the syntax for every data request.

Click on observación-convencional and then /api/observacion/convencional/todas.

Now click on the red sign on the right, where you'll paste and authorize the API key.

Now press the Try it out! button and you'll get:

  • a curl request example
  • the request URL
  • the body, code and headers of the response

If we open the URL inside the "datos" field we'll see the full list of data for all the stations. A JSON lot like this:

Loading...

The main fields are the following:

  • idema: station ID or code.
  • lat and lon: latitude and longitude coordinates.
  • alt: station altitude.
  • fint: data interval date.
  • prec: precipitation in this period.
  • vv: wind speed in the station.
  • ta: ambience temperature in the station.

Data request using javascript

HTML structure

I'll develop an example to access the data and make use of them.

I'll simply create a .html document with a request button, a text line and a request function:

Click the button to request data

<html>
<body>
<button id="solicitud" onclick="solicitar()">Request</button>
<p id="texto">Click the button to request data</p>
</body>
<script>
function solicitar(){
document.getElementById("texto").innerHTML="I didn't program that yet..";
}
</script>
</html>

TIP! Copy and paste the code above in a text document with .html extension and open it in your browser.

HTTP request

We'll do the data request in javascript using the XMLHttpRequest object. The example from w3schools is really simple:

https://www.w3schools.com/js/js_ajax_http.asp

As we saw in the dynamic documentation, after doing the request, we got a link to the data, which is another request, no we need to nest two requests like this:

<script>
function solicitar(){
// Define loading message
document.getElementById("texto").innerHTML="Cargando datos...";
// Define API Key
var AK=tUApiKey;
// Define request URLs
var URL1="https://opendata.aemet.es/opendata/api/observacion/convencional/todas";
var URL2="";

// Create XMLHttpRequest objects
const xhttp1 = new XMLHttpRequest();
const xhttp2 = new XMLHttpRequest();

// Define response function for the first request:
// We want to access te "datos" URL
xhttp1.onload = function() {
  // 1º Parse the response to JSON:
  URL2=JSON.parse(this.responseText);
  // 2º Get the "datos" field:
  URL2=URL2.datos;  
  // 3º Make the new request:
  xhttp2.open("GET", URL2);
  xhttp2.send();
}

// Define response function for the second request:
// We'll modify the text line with some information in the data
xhttp2.onload = function() {
  // 1º Parse the response to JSON (much better to work with):
  var datos=JSON.parse(this.responseText);

  // 2º Get the length of the JSON data
  // This is equivalent to the individual hourly entries per stations
  var registros=datos.length;

  // 3º Get first entry date.
  // We'll format it as it's in ISO standard
  var fechaini=datos[0].fint;
  fechaini=new Date(fechaini+"Z");
  fechaini=fechaini.toLocaleDateString()+" "+fechaini.toLocaleTimeString();

  // 4º Get last entry date.
  // We'll format in the same way
  var fechafin=datos[(datos.length-1)].fint;
  fechafin=new Date(fechaini+"Z");
  fechafin=fechafin.toLocaleDateString()+" "+fechafin.toLocaleTimeString();

  // 5º Merge it all and display in the text line
  document.getElementById("texto").innerHTML="Se han descargado <b>"+registros+"</b> registros desde el <b>"+fechaini+"</b> hasta el <b>"+fechafin+"</b>. <a href='"+URL2+"' target='_blank'>Ver todos los datos.</a>";
}

// Send the first request
xhttp1.open("GET", URL1+"/?api_key="+AK);
xhttp1.send();

}
</script>

Let's see the result below:

Click the button to request data

And done! This way you can obtain coordinates and values for different metheorological stations from AEMET and drop them in a table or web map like we've seen for Leaflet here.

Leave any comments or queries on Twitter!🐦

🐦 @RoamingWorkshop

CesiumJS: the free 3D map viewer for your website

In July, Nature published a high definition Digital Surface Model of the new orography built by La Palma’s Tajogaite volcano eruption in 2021. A spectacular job by Istituto Nazionale di Geofisica e Vulcanologia together with Instituto Vulcanológico de Canarias.

The usual way to see and work with these files is using a GIS software like QGIS or ArcGIS, but how can we make this friendly to standard users? Wouldn’t it be better seen directly in 3D and visit the place virtually?

At this point is where I found CesiumJS: a 3D map viewer styling [G] Earth but open-source, free and highly customizable to make your own projects.

CesiumJS demo viewer of the new Tajogaite terrain with IDE Canarias satellite imagery. Click on the map to interact or open the full version.

Create a Cesium ion account

To use CesiumJS you need to register an account in order to obtain an Access Token.

Registering, we can also upload our own files to customize our 3D map creations. In my case you can see that I uploaded the Digital Surface Model and imagery of the viewer. I drop the links in case you want to use the same files:

TIP! You can use the WMS service in your GIS software to extract the imagery, or download the one I use in this example (270 MB with 1m resolution)

Include CesiumJS in your web.

To use CesiumJS we configure a .html file as usual.

In this case, I'll copy the code directly from their quickstart example as it is only about 25 lines:

As you see, the main parts are:

  • links to CesiumJS libraries in the <head>
  • a <div> element with id "cesiumContainer" inside the <body>
  • the Access Token as variable Cesium.Ion.defaultAccessToken. Introduce yours here.
  • a "viewer" variable
  • the initial viewer configuration, specifying initial coordinates and X, Z angles.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <!-- Include the CesiumJS JavaScript and CSS files -->
  <script src="https://cesium.com/downloads/cesiumjs/releases/1.97/Build/Cesium/Cesium.js"></script>
  <link href="https://cesium.com/downloads/cesiumjs/releases/1.97/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>
<body>
  <div id="cesiumContainer"></div>
  <script>
    // Your access token can be found at: https://cesium.com/ion/tokens.
    // This is the default access token from your ion account
    Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1MTYwNGM0Yi1iYzlkLTRkMTUtOGQyOS04YjQxNmUwZDQ0YjkiLCJpZCI6MTA0Mjc3LCJpYXQiOjE2NjAxMDk4MzR9.syNWeKPLWA2eMrEh4K9hvkyp1oGdbLMaL0Ozk1kaksk';
    // Initialize the Cesium Viewer in the HTML element with the `cesiumContainer` ID.
    const viewer = new Cesium.Viewer('cesiumContainer', {
      terrainProvider: Cesium.createWorldTerrain()
    });    
    // Add Cesium OSM Buildings, a global 3D buildings layer.
    const buildingTileset = viewer.scene.primitives.add(Cesium.createOsmBuildings());   
    // Fly the camera to San Francisco at the given longitude, latitude, and height.
    viewer.camera.flyTo({
      destination : Cesium.Cartesian3.fromDegrees(-122.4175, 37.655, 400),
      orientation : {
        heading : Cesium.Math.toRadians(0.0),
        pitch : Cesium.Math.toRadians(-15.0),
      }
    });
  </script>
 </div>
</body>
</html>

TIP! You can also donwload CesiumJS libraries and use them locally or in your server

Add a custom terrain

Follow the steps in the Cesium page:

  • In your Cesium ion account, add the terrain as a new asset with Add data.
  • Select the file and the data type as Raster Terrain
  • Inside Terrain Options, select the base terrain as Main Sea Level and leave the rest as default.
  • When the file is uploaded, you'll find a code sample to use this new terrain.

Add imagery

Repeating the previous step, now upload the satellite imagery and choose Raster Imagery data type.

In this case, we also get a sample code to use this image.

Customizing the viewer

Add all the above to your .html file and customize it to your like. For example, you can:

  • Adjust container elements so they fill the whole screen:
<body style="margin:0;width:100vw;height:100vh;">
<div style="height:100%;" id="cesiumContainer"></div>
  • Hide bottom controls:
viewer.timeline.container.style.visibility = "hidden";
viewer.animation.container.style.visibility = "hidden";
  • Adjust initial view coordinates to our site:
viewer.camera.flyTo({
    destination : Cesium.Cartesian3.fromDegrees(-17.8195, 28.6052, 3000),
    orientation : {
    heading : Cesium.Math.toRadians(-85.0),
    pitch : Cesium.Math.toRadians(-30.0),
    }
});

This is the result you might have seen already:

And its full code:

<html lang="en">
<head>
<meta charset="utf-8">

<script src="https://cesium.com/downloads/cesiumjs/releases/1.96/Build/Cesium/Cesium.js"></script>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.96/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>
<body style="margin:0;width:100vw;height:100vh;">
<div style="height:100%;" id="cesiumContainer"></div>
<script>
    // Your access token can be found at: https://cesium.com/ion/tokens.
    // Replace `your_access_token` with your Cesium ion access token.

    Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1MTYwNGM0Yi1iYzlkLTRkMTUtOGQyOS04YjQxNmUwZDQ0YjkiLCJpZCI6MTA0Mjc3LCJpYXQiOjE2NjAxMDk4MzR9.syNWeKPLWA2eMrEh4K9hvkyp1oGdbLMaL0Ozk1kaksk';

    // Initialize the Cesium Viewer in the HTML element with the `cesiumContainer` ID.
    const viewer = new Cesium.Viewer('cesiumContainer', {
      //terrainProvider: Cesium.createWorldTerrain()//terreno original
		terrainProvider: new Cesium.CesiumTerrainProvider({//terreno modificado
			url: Cesium.IonResource.fromAssetId(1255858),
		}),	  
    });    
	
	// Esconder widgest inferiores
	viewer.timeline.container.style.visibility = "hidden";
	viewer.animation.container.style.visibility = "hidden";
	
	// Add Sentinel2 imagery
	const layer = viewer.imageryLayers.addImageryProvider(
	  new Cesium.IonImageryProvider({ assetId: 1256129 })
	);
    // Add Cesium OSM Buildings, a global 3D buildings layer.
    const buildingTileset = viewer.scene.primitives.add(Cesium.createOsmBuildings());   
    // Fly the camera to San Francisco at the given longitude, latitude, and height.
    viewer.camera.flyTo({
      destination : Cesium.Cartesian3.fromDegrees(-17.8195, 28.6052, 3000),
      orientation : {
        heading : Cesium.Math.toRadians(-85.0),
        pitch : Cesium.Math.toRadians(-30.0),
      }
    });
  </script>
</div>
</html>

And that's it! You can create tons of immersive web apps with detailed terrains better than the standard ones.

Where else can you use CesiumJS? Tell it on Twitter 🐦

🐦 @RoamingWorkshop

Interactive text editor for your web apps with Rich Text Editor

One of my latest projects is a notes app that is hosted in my server so I can stop using [G] Notes.

An interesting feature that I am including is Rich Text Editor, a rich javascript library in the form of user interface to format HTML text of any web form.

It’s open source, free for personal use, and very simple to use. A true win-win that I’m showing you today.

License

I said Rich Text Editor is free, and it is for a non-commercial use.

It's important that you read their pricing page to understand the limitations of the free license:

  • 1 developer
  • 1 domain
  • 5 active users

In my case, I installed Rich Text Editor in this server that hosts the blog, and it's been restricted with php so it's only accessible from the same domain.

TIP! Do NOT host the files anywhere accessible for anyone (like a public server), as you might lead to a license break, being forced to pay a standard license or retiring your app.

Anyway, commercial licenses are one-off payments and it might be worth getting one if you're going to give it plenty of use.

Installation

Using Rich Text Editor is very simple and it's explained in their document page.

To install Rich Text Editor you only need to download the files from their website in a local folder or in your web server.

https://richtexteditor.com/download.aspx

Enable interface

Rich Text Editor works inside an HTML document.

Build it like this:

  1. As always, create an HTML file with the basic structure.
  2. Link the scripts and CSS files in the <head> :

    <link rel="stylesheet" href="/richtexteditor/rte_theme_default.css" />  
    <script type="text/javascript" src="/richtexteditor/rte.js"></script>  
    <script type="text/javascript" src='/richtexteditor/plugins/all_plugins.js'></script>  

  3. Inside the <body> add the following <div> :

    <div id="div_editor1" > 
    <p>Initial Document Content</p> 
    </div>

  4. Finally, call the editor in the <script> :

    var editor1 = new RichTextEditor("#div_editor1", {skin:"rounded-corner", toolbar:"full"});

All the above would look like this (you can interact with it):

And the full code is the following:

<html>
<head>

<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css" />
<script type="text/javascript" src="/richtexteditor/rte.js"></script>
<script type="text/javascript" src='/richtexteditor/plugins/all_plugins.js'></script

</head>
<body>

<div id="div_editor1" > 
<p>Initial Document Content</p> 
</div> 

</body>
<script>

var editor1 = new RichTextEditor("#div_editor1", {skin:"rounded-corner", toolbar:"full"});

</script>
</html>

TIP! Copy and paste the above code to a local .html file inside the richtexteditor folder, and you can see the example in your browser.

Configuration

As you can see, I added some properties when calling the editor:

{skin:"rounded-corner", toolbar:"full"}

There are plenty of configuration options that you can check here:

https://richtexteditor.com/docs/configuration-reference.aspx

Editor functions

The library includes multiple functions to interact with the editor. All the command available can be found here:

https://richtexteditor.com/docs/cmd_allcommands.aspx

One basic funcionality is to capture the full HTML code for the text that we have been editing. That's what the button at the bottom does, using the getHTMLcode():

<button onclick="alert(editor1.getHTMLCode())">Show Html Code</button>

Uses

As stated previously, I'll be using this library as an editor for a notes app that will be hosted in my server.

The editor will let me easily add nice formatting to the notes and save them with the full format code.

Here you have a demo of how the interface is looking, although there are no functions enabled so far.

Other applications where you can use this editor are self-made forums, user forms, comments boxes or any text-box where a user needs to input text in your web app.

I hope you found this useful as I did and give it good use. Any doubts or comments, drop them on Twitter 🐦 See you soon!

🐦 @RoamingWorkshop

Custom Widgets for your WordPress site

To avoid the use of unnecessary external plugins, here you’ll see how to make your own widgets for your WordPress site or to insertthem in any post.

All you need to know is some HTML and javascript. Let’s get down to it!

Custom HTML

If you've seen any other post in this blog, you've seen the table of contents above, or the cookie message to the right.

Both are dynamic elements programmed in HTML and javascript which you can insert anywhere in your WordPress site using a "Custom HTML" block.

Just insert this type of block and inside you can introduce a customizable HTML and program with javascript. Let's see a couple of examples.

The sidebar in this blog shows a cookie consent message with a hyperlink to the privacy policy.

You might have noticed that the content changes automatically when you switch languages.

Right click to inspect the website structure.

For it to work properly, imagine that the Custom HTML block is an iframe or some kind on sub-site inside this website.

You can edit the content of the Custom HTML as if it was an external site, and freely add anything you want.

For the cookie notice I just want a line of text, and I assign "cookie-txt" as an id. It is good to add the HTML structure, but you can avoid most of it.

<html>
<body>
<p id="cookie-txt"></p>
</body>
</html>

Now I add a javascript function which reads the site language and fills this line according to it.

You can do different tests using the inspector and console of your browser, right-clicking in the webpage.

A fast way to get the language is reading the lang property of the <html> element of the site.

The rest is a condition:

  • If it detects "es-ES", message is defined in Spanish.
  • If it detects "en-GB", message is defined in English.

The content of the message is defined in the innerHTML property of the <p> element, so it can be formatted with HTML tags adding a hyperlink or bold text.

<script>
var lang=document.getElementsByTagName("html")[0].lang;
if(lang=="es-ES"){
document.getElementById("cookie-txt").innerHTML="🍪 <a href='https://theroamingworkshop.cloud/b/?page_id=1225' target='_blank'>Política</a> <b>anti-Cookies</b> aceptada al navegar.</p>";
}else if(lang=="en-GB"){
document.getElementById("cookie-txt").innerHTML="<b>🍪 anti-Cookies</b> <a href='https://theroamingworkshop.cloud/b/?page_id=3' target='_blank'>policy</a> accepted while browsing.";
}
</script>

It would look like this:

The full code of the Custom HTML block showing the cookie notice is the following:

<html>
<body>
<p id="cookie-txt"></p>
</body>
<script>
var lang=document.getElementsByTagName("html")[0].lang;
if(lang=="es-ES"){
document.getElementById("cookie-txt").innerHTML="🍪 <a href='https://theroamingworkshop.cloud/b/?page_id=1225' target='_blank'>Política</a> <b>anti-Cookies</b> aceptada al navegar.</p>";
}else if(lang=="en-GB"){
document.getElementById("cookie-txt").innerHTML="<b>🍪 anti-Cookies</b> <a href='https://theroamingworkshop.cloud/b/?page_id=3' target='_blank'>policy</a> accepted while browsing.";
}
</script>
</html>

In this case, the block is inserted as an element in the sidebar:

Table of contents

The table of contents that appears in every post is done in the same way.

Firstly, I will define the table giving it some format and leaving the content empty so it's filled later by a function.

<div id="menu" style="padding:20px 20px 20px 20px; border-left:2px solid darkgrey;">
<p style="font-weight:bold;">Contenido</p>
</div>

In this case, I will search for "headings" or titles of each section, specifically for <h2> tags. For this reason, when writing every post I must always use this type of heading (which is default) if I want it to appear in the table.

Additionally, I define the html Anchor property, which will assign and "id" that will allow to create a hyperlink that takes us to that heading.

Then a function will do the following:

  1. Search for <h2> tags one by one.
    for (let i=0;i < window.document.getElementsByTagName("h2").length; i++) { element = window.document.getElementsByTagName("h2")[i];
    ...
  2. Get the text inside.
    text = "▹ "+element.innerHTML;
  3. Create a new hyperlink (<a>) element which will go in the table of contents, with the previous text.
    var newelement = document.createElement("a"); newelement.innerHTML=text;
  4. Get the post "id" and heading "id" to create a hyperlink to this heading.
    var postid = window.document.getElementsByTagName("article")[0].id;
    var url = "https://theroamingworkshop.cloud/b/?p="+postid.substr(5,postid.length)+"#"+element.id;
  5. Insert it as a new child in the table of contents.
    newelement.setAttribute("href", url); newelement.appendChild(document.createElement("br")); window.document.getElementById("menu").appendChild(newelement);

TIP! Adding #section-id at the end of any URL it will take you to such section.

  1. Finally, some pages take a bit of time to load and the headings wont be available, so I'll add the previous code inside a function and call it using a timeout.
    window.setTimeout(fillmenu,500);

The full code for the block will be like this:

<div id="menu" style="padding:20px 20px 20px 20px; border-left:2px solid darkgrey;">
<p style="font-weight:bold;">Contenido</p>
</div>
<script>
var text;
var element;

function fillmenu(){

for (let i=0; i<window.document.getElementsByTagName("h2").length; i++){
element = window.document.getElementsByTagName("h2")[i];
text = "▹ "+element.innerHTML;
var newelement = document.createElement("a");
newelement.innerHTML=text;
var postid=window.document.getElementsByTagName("article")[0].id;
var url = "https://theroamingworkshop.cloud/b/?p="+postid.substr(5,postid.length)+"#"+element.id;
newelement.setAttribute("href", url);
newelement.appendChild(document.createElement("br"));
window.document.getElementById("menu").appendChild(newelement);
}
}

window.setTimeout(fillmenu,500);

</script>

TIP! Add this HTML block to your reusables and you can insert it in any other post.

TIP! block

TIP! This TIP! block is another example. You'll have to convert it back to a regular block if you want to change the content only in this inserted block.

Here's the code:

<p style="border-left:3px solid orange;padding-left:5px;font-size:14px;"><i><b>TIP! </b>This <b>TIP!</b> block is another example. You'll have to convert it back to a regular block if you want to change the content <b>only</b> in this inserted block.</i></p>

Conclusion

As you can see, there are unlimited possibilities and it's up to your imagination.

Be creative and design your own widgets, they will add value to your blog!

As always, doubts or comments on Twitter 🐦 See you soon!

(the Twitter block is also a reusable 👀 )

🐦 @RoamingWorkshop

« Older posts Newer posts »

© 2025 The Roaming Workshop

Theme by Anders NorénUp ↑