Live now!

Progress of current post

Create a Logo based Stats Image with Sportmonks’ Football API and Groovy

While the word fashionable or exciting comes to mind when you come across the term Groovy, in programming parlance it has somewhat of a different meaning.

Apache Groovy is a powerful, optionally typed, and dynamic programming language with static typing and static compilation capabilities for the Java platform aimed at improving developer productivity.

Published 17 December 2024
Last updated 31 December 2024
Wesley Van Rooij
Create a Logo based Stats Image with Sportmonks’ Football API and Groovy
Contents

With its familiar syntax, it integrates smoothly with any Java program and can make use of several Java extensions.

In today’s guide we will write a complete piece of code in Groovy to generate an image with match statistics on any game of your choice.

Why Use Apache Groovy?

Although it sits in 25th place on popular programming languages according to a 2023 survey by Stackoverflow, Groovy is actively being used by several notable names, such as LinkedIn. Popular payment system Mastercard, music entertainment channel MTV, and software titans IBM, Oracle, and Google are all users of Apache Groovy, not to mention your favourite movie streaming platform, Netflix.

Thanks to a vibrant and rich ecosystem, a flat learning curve, and easy Java integration, among several other qualities, Apache Groovy remains an ideal choice for JSON manipulation.

Groovy comes with integrated support for converting between its objects and JSON, thanks to a package called groovy.json, which we shall use in this project.

Sportmonks & Apache Groovy.

Just like we did in the previous episode of our guide using Julia, at the end of this blog post, you’ll have an image showing match stats for any match of your choice.

In the upcoming sections, we’ll walk you through configuring your development environment before making your first call to the API.

As we drill down to stats for a specific fixture, each segment will build on the next, showcasing examples and code snippets.

Chapter 1: Setting up your environment

Before we get started, we will set up an environment to write and compile Groovy. After testing a handful of IDEs, we found JetBrains IntelliJ IDEA to be the easiest to work with. It also comes with a boatload of plugins and extensions in the marketplace.

1.1 Installing IntelliJ IDEA.

IntelliJ IDEA is a cross-platform IDE that provides a consistent experience on the Windows, macOS, and Linux operating systems.

You should have IntelliJ IDEA already installed on your workstation if you read our previous guide where we deployed Kotlin extensively.

If not, kindly download IntelliJ IDEA Community Edition, which is completely free to use.

We will set up our project in the next chapter after installing relevant plugins.

1.2 Obtaining an API Token

To use the API, sign up for an account on https://my.sportmonks.com. Your account automatically comes with the Danish Super Liga and Scottish Premier League plans, which are free forever.

However, for today’s exercise, you can take advantage of the 14-day free trial on any of the other plans if you haven’t done so previously.

Create an API token on the dashboard, which you must keep private. In the event that your API token becomes compromised, delete it and create a new one. An API token is required to authenticate our requests.

With your API token ready and your IDE installed, we can begin.

1.3 Create a Groovy Project.

We installed IntelliJ IDEA earlier on; however, to write and compile Groovy, we must install the required plugin and JDK version in our project.

Groovy-Project-2

As you can see in the image above, Groovy is not listed on the upper left side of IntelliJ. Search for Groovy via the ‘More via Plugins’ at the bottom left corner and install the plugin. If you encounter any problem, you may visit the official guide for help.

Once done, create a new project following the earlier steps.

Groovy-Project-1

Now you will be able to find Groovy on the left-hand side.

Groovy-Project-3

Enter the name of your project. However, an important step is selecting the proper version of Java. This is a common problem which could leave you tearing your hair out.

Groovy might be running on a version of Java which may be incompatible with the libraries or itself; hence, you must use a compatible JDK version. For Groovy 3.x, it requires Java 8-16, while for Groovy 4.x, it is compatible with Java 8-20. This may change in the future depending on the time you are reading this blog post, so always refer to the official help page shared earlier.

At this moment, since our version is Groovy 4.0.14, we will select JDK version 19. Once selected, proceed to have all libraries downloaded and navigate to your Main.groovy file, which comes with the legendary ‘Hello World’ example.

We will replace this sample with another piece of code in the next section.

Chapter 2: Retrieving Match Stats.

We will jump straight into retrieving match stats from a fixture of choice in this exercise.

However, if you would like to learn about how we adopted ‘filters’ and ‘includes’ to narrow down to specific stats of interest, you can read all about this in our previous blog with Julia.

2.2 Retrieving Stats using Includes.

Using an include statement with ‘statistics.type,’ we will retrieve all the match stats from the fixture ID 19134600, which was the game between Manchester City and Tottenham.

That fixtured ended in a 4-0 loss for the Sky Blues. It was Pep Guardiola’s record fifth successive defeat of his managerial career.

Using our URL of interest, we will write a piece of code in Groovy to retrieve the stats.

https://api.sportmonks.com/v3/football/fixtures/FixtureID?api_token=API_Token&include=statistics.type

import groovy.json.JsonSlurper 
import groovy.json.JsonOutput 
import java.net.HttpURLConnection 
import java.net.URL 
 
def fetchData(apiUrl) { 
  try { 
    // Create a connection to the API URL 
    def url = new URL(apiUrl) 
    HttpURLConnection connection = (HttpURLConnection) url.openConnection() 
    connection.setRequestMethod("GET") 
    connection.setConnectTimeout(5000) 
    connection.setReadTimeout(5000) 
 
    // Read the response 
    def response = connection.inputStream.text 
 
    // Parse the JSON response 
    def json = new JsonSlurper().parseText(response) 
 
    // Pretty print the JSON 
    def prettyJson = JsonOutput.prettyPrint(JsonOutput.toJson(json)) 
    println("Data:\n${prettyJson}") 
 
    return json 
  } catch (Exception e) { 
    println("Error: ${e.message}") 
  } 
  return null 
} 
 
// Provide the API URL 
def apiUrl = "https://api.sportmonks.com/v3/football/fixtures/19134600?api_token=API_Token&include=statistics.type" 
 
// Fetch and pretty print the data 
fetchData(apiUrl) 
Data:
{
    "data": {
        "id": 19134600,
        "sport_id": 1,
        "league_id": 8,
        "season_id": 23614,
        "stage_id": 77471288,
        "group_id": null,
        "aggregate_id": null,
        "round_id": 339246,
        "state_id": 5,
        "venue_id": 151,
        "name": "Manchester City vs Tottenham Hotspur",
        "starting_at": "2024-11-23 17:30:00",
        "result_info": "Tottenham Hotspur won after full-time.",
        "leg": "1/1",
        "details": null,
        "length": 90,
        "placeholder": false,
        "has_odds": true,
        "has_premium_odds": true,
        "starting_at_timestamp": 1732383000,
        "statistics": [
            {
                "id": 154543270,
                "fixture_id": 19134600,
                "type_id": 56,
                "participant_id": 6,
                "data": {
                    "value": 9
                },
                "location": "away",
                "type": {
                    "id": 56,
                    "name": "Fouls",
                    "code": "fouls",
                    "developer_name": "FOULS",
                    "model_type": "statistic",
                    "stat_group": "defensive"
                }
            },
            {
                "id": 154461825,
                "fixture_id": 19134600,
                "type_id": 42,
                "participant_id": 9,
                "data": {
                    "value": 23
                },
                "location": "home",
                "type": {
                    "id": 42,
                    "name": "Shots Total",
                    "code": "shots-total",
                    "developer_name": "SHOTS_TOTAL",
                    "model_type": "statistic",
                    "stat_group": "offensive"
                }
            },
            {
                "id": 154543030,
                "fixture_id": 19134600,
                "type_id": 80,
                "participant_id": 9,
                "data": {
                    "value": 566
                },
                "location": "home",
                "type": {
                    "id": 80,
                    "name": "Passes",
                    "code": "passes",
                    "developer_name": "PASSES",
                    "model_type": "statistic",
                    "stat_group": "overall"
                }
            },
            {
                "id": 154541889,
                "fixture_id": 19134600,
                "type_id": 86,
                "participant_id": 6,
                "data": {
                    "value": 7
                },
                "location": "away",
                "type": {
                    "id": 86,
                    "name": "Shots On Target",
                    "code": "shots-on-target",
                    "developer_name": "SHOTS_ON_TARGET",
                    "model_type": "statistic",
                    "stat_group": "offensive"
                }
            },
            {
                "id": 154541880,
                "fixture_id": 19134600,
                "type_id": 86,
                "participant_id": 9,
                "data": {
                    "value": 5
                },
                "location": "home",
                "type": {
                    "id": 86,
                    "name": "Shots On Target",
                    "code": "shots-on-target",
                    "developer_name": "SHOTS_ON_TARGET",
                    "model_type": "statistic",
                    "stat_group": "offensive"
                }
            },
            {
                "id": 154543137,
                "fixture_id": 19134600,
                "type_id": 56,
                "participant_id": 9,
                "data": {
                    "value": 19
                },
                "location": "home",
                "type": {
                    "id": 56,
                    "name": "Fouls",
                    "code": "fouls",
                    "developer_name": "FOULS",
                    "model_type": "statistic",
                    "stat_group": "defensive"
                }
            },
            {
                "id": 154511651,
                "fixture_id": 19134600,
                "type_id": 45,
                "participant_id": 9,
                "data": {
                    "value": 58
                },
                "location": "home",
                "type": {
                    "id": 45,
                    "name": "Ball Possession %",
                    "code": "ball-possession",
                    "developer_name": "BALL_POSSESSION",
                    "model_type": "statistic",
                    "stat_group": "overall"
                }
            },
            {
                "id": 154511652,
                "fixture_id": 19134600,
                "type_id": 45,
                "participant_id": 6,
                "data": {
                    "value": 42
                },
                "location": "away",
                "type": {
                    "id": 45,
                    "name": "Ball Possession %",
                    "code": "ball-possession",
                    "developer_name": "BALL_POSSESSION",
                    "model_type": "statistic",
                    "stat_group": "overall"
                }
            },
            {
                "id": 154461826,
                "fixture_id": 19134600,
                "type_id": 42,
                "participant_id": 6,
                "data": {
                    "value": 9
                },
                "location": "away",
                "type": {
                    "id": 42,
                    "name": "Shots Total",
                    "code": "shots-total",
                    "developer_name": "SHOTS_TOTAL",
                    "model_type": "statistic",
                    "stat_group": "offensive"
                }
            },
            {
                "id": 154543328,
                "fixture_id": 19134600,
                "type_id": 80,
                "participant_id": 6,
                "data": {
                    "value": 427
                },
                "location": "away",
                "type": {
                    "id": 80,
                    "name": "Passes",
                    "code": "passes",
                    "developer_name": "PASSES",
                    "model_type": "statistic",
                    "stat_group": "overall"
                }
            }
        ]
    },
    "subscription": [
        {
            "meta": {
                "trial_ends_at": "2024-10-16 14:50:45",
                "ends_at": null,
                "current_timestamp": 1734483733
            },
            "plans": [
                {
                    "plan": "Enterprise plan (loyal)",
                    "sport": "Football",
                    "category": "Advanced"
                },
                {
                    "plan": "Enterprise Plan",
                    "sport": "Cricket",
                    "category": "Standard"
                },
                {
                    "plan": "Formula One",
                    "sport": "Formula One",
                    "category": "Standard"
                }
            ],
            "add_ons": [
                {
                    "add_on": "All-in News API",
                    "sport": "Football",
                    "category": "News"
                },
                {
                    "add_on": "pressure index add-on",
                    "sport": "Football",
                    "category": "Default"
                },
                {
                    "add_on": "Enterprise Plan Predictions",
                    "sport": "Football",
                    "category": "Predictions"
                },
                {
                    "add_on": "xG Advanced",
                    "sport": "Football",
                    "category": "Expected"
                }
            ],
            "widgets": [
                {
                    "widget": "Sportmonks Widgets",
                    "sport": "Football"
                }
            ]
        }
    ],
    "rate_limit": {
        "resets_in_seconds": 66,
        "remaining": 2986,
        "requested_entity": "Fixture"
    },
    "timezone": "UTC"
}

Explanation

HttpURLConnection is the core Java class used for HTTP requests, which supports GET, POST, PUT, DELETE, and more HTTP methods.

JsonSlurper parses the JSON response into a Groovy object (Map/List), while JsonOutput.prettyPrint converts the parsed JSON object back to a well-formatted string to enable a clear and readable format.

2.3. Filter Match stats for specific details.

As you can see, we retrieved a lengthy response. However, what do we do if we need to display just a few of them? Using a tool named ‘Filters,’ we can easily narrow down the response to just a few.

By looking through the response, we will filter the response using five different type_ids, namely, 45 [Ball possession], 86 [Shots on Target], 42 [Shots], 80 [Passes], and 56 [Fouls].

We shall append these type_ids, separated by commas, at the end of the previous url in this form: ‘fixtureStatisticTypes:type_id,type_id,…’

https://api.sportmonks.com/v3/football/fixtures/19134600?api_token=API_Token&include=statistics.type&filters=fixtureStatisticTypes:45,86,42,80,56

import groovy.json.JsonSlurper 
import groovy.json.JsonOutput 
import java.net.HttpURLConnection 
import java.net.URL 
 
def fetchData(apiUrl) { 
  try { 
    // Create a connection to the API URL 
    def url = new URL(apiUrl) 
    HttpURLConnection connection = (HttpURLConnection) url.openConnection() 
    connection.setRequestMethod("GET") 
    connection.setConnectTimeout(5000) 
    connection.setReadTimeout(5000) 
 
    // Read the response 
    def response = connection.inputStream.text 
 
    // Parse the JSON response 
    def json = new JsonSlurper().parseText(response) 
 
    // Pretty print the JSON 
    def prettyJson = JsonOutput.prettyPrint(JsonOutput.toJson(json)) 
    println("Data:\n${prettyJson}") 
 
    return json 
  } catch (Exception e) { 
    println("Error: ${e.message}") 
  } 
  return null 
} 
 
// Provide the API URL 
def apiUrl = "https://api.sportmonks.com/v3/football/fixtures/19134600?api_token=API_Token&include=statistics.type&filters=fixtureStatisticTypes:45,86,42,80,56" 
 
// Fetch and pretty print the data 
fetchData(apiUrl) 

Data:
{
    "data": {
        "id": 19134600,
        "sport_id": 1,
        "league_id": 8,
        "season_id": 23614,
        "stage_id": 77471288,
        "group_id": null,
        "aggregate_id": null,
        "round_id": 339246,
        "state_id": 5,
        "venue_id": 151,
        "name": "Manchester City vs Tottenham Hotspur",
        "starting_at": "2024-11-23 17:30:00",
        "result_info": "Tottenham Hotspur won after full-time.",
        "leg": "1/1",
        "details": null,
        "length": 90,
        "placeholder": false,
        "has_odds": true,
        "has_premium_odds": true,
        "starting_at_timestamp": 1732383000,
        "statistics": [
            {
                "id": 154543270,
                "fixture_id": 19134600,
                "type_id": 56,
                "participant_id": 6,
                "data": {
                    "value": 9
                },
                "location": "away",
                "type": {
                    "id": 56,
                    "name": "Fouls",
                    "code": "fouls",
                    "developer_name": "FOULS",
                    "model_type": "statistic",
                    "stat_group": "defensive"
                }
            },
            {
                "id": 154461825,
                "fixture_id": 19134600,
                "type_id": 42,
                "participant_id": 9,
                "data": {
                    "value": 23
                },
                "location": "home",
                "type": {
                    "id": 42,
                    "name": "Shots Total",
                    "code": "shots-total",
                    "developer_name": "SHOTS_TOTAL",
                    "model_type": "statistic",
                    "stat_group": "offensive"
                }
            },
            {
                "id": 154543030,
                "fixture_id": 19134600,
                "type_id": 80,
                "participant_id": 9,
                "data": {
                    "value": 566
                },
                "location": "home",
                "type": {
                    "id": 80,
                    "name": "Passes",
                    "code": "passes",
                    "developer_name": "PASSES",
                    "model_type": "statistic",
                    "stat_group": "overall"
                }
            },
            {
                "id": 154541889,
                "fixture_id": 19134600,
                "type_id": 86,
                "participant_id": 6,
                "data": {
                    "value": 7
                },
                "location": "away",
                "type": {
                    "id": 86,
                    "name": "Shots On Target",
                    "code": "shots-on-target",
                    "developer_name": "SHOTS_ON_TARGET",
                    "model_type": "statistic",
                    "stat_group": "offensive"
                }
            },
            {
                "id": 154541880,
                "fixture_id": 19134600,
                "type_id": 86,
                "participant_id": 9,
                "data": {
                    "value": 5
                },
                "location": "home",
                "type": {
                    "id": 86,
                    "name": "Shots On Target",
                    "code": "shots-on-target",
                    "developer_name": "SHOTS_ON_TARGET",
                    "model_type": "statistic",
                    "stat_group": "offensive"
                }
            },
            {
                "id": 154543137,
                "fixture_id": 19134600,
                "type_id": 56,
                "participant_id": 9,
                "data": {
                    "value": 19
                },
                "location": "home",
                "type": {
                    "id": 56,
                    "name": "Fouls",
                    "code": "fouls",
                    "developer_name": "FOULS",
                    "model_type": "statistic",
                    "stat_group": "defensive"
                }
            },
            {
                "id": 154511651,
                "fixture_id": 19134600,
                "type_id": 45,
                "participant_id": 9,
                "data": {
                    "value": 58
                },
                "location": "home",
                "type": {
                    "id": 45,
                    "name": "Ball Possession %",
                    "code": "ball-possession",
                    "developer_name": "BALL_POSSESSION",
                    "model_type": "statistic",
                    "stat_group": "overall"
                }
            },
            {
                "id": 154511652,
                "fixture_id": 19134600,
                "type_id": 45,
                "participant_id": 6,
                "data": {
                    "value": 42
                },
                "location": "away",
                "type": {
                    "id": 45,
                    "name": "Ball Possession %",
                    "code": "ball-possession",
                    "developer_name": "BALL_POSSESSION",
                    "model_type": "statistic",
                    "stat_group": "overall"
                }
            },
            {
                "id": 154461826,
                "fixture_id": 19134600,
                "type_id": 42,
                "participant_id": 6,
                "data": {
                    "value": 9
                },
                "location": "away",
                "type": {
                    "id": 42,
                    "name": "Shots Total",
                    "code": "shots-total",
                    "developer_name": "SHOTS_TOTAL",
                    "model_type": "statistic",
                    "stat_group": "offensive"
                }
            },
            {
                "id": 154543328,
                "fixture_id": 19134600,
                "type_id": 80,
                "participant_id": 6,
                "data": {
                    "value": 427
                },
                "location": "away",
                "type": {
                    "id": 80,
                    "name": "Passes",
                    "code": "passes",
                    "developer_name": "PASSES",
                    "model_type": "statistic",
                    "stat_group": "overall"
                }
            }
        ]
    },
    "subscription": [
        {
            "meta": {
                "trial_ends_at": "2024-10-16 14:50:45",
                "ends_at": null,
                "current_timestamp": 1734485131
            },
            "plans": [
                {
                    "plan": "Enterprise plan (loyal)",
                    "sport": "Football",
                    "category": "Advanced"
                },
                {
                    "plan": "Enterprise Plan",
                    "sport": "Cricket",
                    "category": "Standard"
                },
                {
                    "plan": "Formula One",
                    "sport": "Formula One",
                    "category": "Standard"
                }
            ],
            "add_ons": [
                {
                    "add_on": "All-in News API",
                    "sport": "Football",
                    "category": "News"
                },
                {
                    "add_on": "pressure index add-on",
                    "sport": "Football",
                    "category": "Default"
                },
                {
                    "add_on": "Enterprise Plan Predictions",
                    "sport": "Football",
                    "category": "Predictions"
                },
                {
                    "add_on": "xG Advanced",
                    "sport": "Football",
                    "category": "Expected"
                }
            ],
            "widgets": [
                {
                    "widget": "Sportmonks Widgets",
                    "sport": "Football"
                }
            ]
        }
    ],
    "rate_limit": {
        "resets_in_seconds": 2791,
        "remaining": 2995,
        "requested_entity": "Fixture"
    },
    "timezone": "UTC"
}

Chapter 3.0 Retrieving only required values.

In this chapter, we modify our code to retrieve only the needed values and include a line naming the home and away teams.

import groovy.json.JsonSlurper

def fetchData(apiUrl) {
    try {
        // Open the URL connection
        def url = new URL(apiUrl)
        HttpURLConnection connection = (HttpURLConnection) url.openConnection()
        connection.setRequestMethod("GET")
        connection.setConnectTimeout(5000)
        connection.setReadTimeout(5000)

        // Check for a successful response
        if (connection.responseCode == 200) {
            // Parse the JSON response
            def response = connection.inputStream.text
            def json = new JsonSlurper().parseText(response)

            // Access the fixture data (home and away team)
            def fixtureName = json.data?.name
            if (fixtureName) {
                def teams = fixtureName.split(" vs ")
                def homeTeam = teams[0]?.trim()
                def awayTeam = teams[1]?.trim()
                def header = String.format("%-25s %-40s %-25s", homeTeam, "----- STATS ----", awayTeam)
                println header

                // Access statistics data
                def statistics = json.data?.statistics
                if (statistics) {
                    def statsLines = []
                    statistics.each { stat ->
                        if (stat.location == 'home') {
                            def typeId = stat.type_id
                            def name = stat.type?.name
                            def homeValue = stat.data?.value
                            def awayStat = statistics.find { it.location == 'away' && it.type_id == typeId }
                            def awayValue = awayStat?.data?.value ?: "N/A"

                            // Format the output to align values and names with equal spaces between them
                            def formattedLine = String.format("%-30s %-42s %-30s", homeValue, name, awayValue)
                            statsLines << formattedLine } } // Center-align each stats line relative to the header def headerWidth = header.length() statsLines.each { line ->
                        int padding = Math.max((int) ((headerWidth - line.length()) / 2), 0) // Explicit cast to `int`
                        println " " * padding + line
                    }
                } else {
                    println "No statistics data found in the response."
                }
            } else {
                println "Fixture name not found in the response."
            }
        } else {
            println "Failed to fetch data. HTTP response code: ${connection.responseCode}"
        }
    } catch (Exception e) {
        println "Error: ${e.message}"
    }
}

// API URL
def apiUrl = "https://api.sportmonks.com/v3/football/fixtures/19134600?api_token=API_Token&include=statistics.type&filters=fixtureStatisticTypes:45,86,42,80,56"

// Fetch and print data
fetchData(apiUrl)

 

Explanation.

Our new code iterates through the statistics for the home and away teams and formats it for neat printing. It also adds the names of both teams right before the stats.

Manchester City —– STATS —- Tottenham Hotspur
23 Shots Total 9
566 Passes 427
5 Shots On Target 7
19 Fouls 9
58 Ball Possession % 42

 

3.1 Creating the Image.

In this section, we will jump through a few hoops to create an image with the text centered properly.

Here is our new piece of code.

import groovy.json.JsonSlurper 
import java.awt.Color 
import java.awt.Font 
import java.awt.Graphics 
import java.awt.FontMetrics 
import java.awt.image.BufferedImage 
import javax.imageio.ImageIO 
 
def fetchData(apiUrl) { 
  try { 
    // Open the URL connection 
    def url = new URL(apiUrl) 
    HttpURLConnection connection = (HttpURLConnection) url.openConnection() 
    connection.setRequestMethod("GET") 
    connection.setConnectTimeout(5000) 
    connection.setReadTimeout(5000) 
 
    // Check for a successful response 
    if (connection.responseCode == 200) { 
      // Parse the JSON response 
      def response = connection.inputStream.text 
      def json = new JsonSlurper().parseText(response) 
 
      // Access the fixture data (home and away team) 
      def fixtureName = json.data?.name 
      if (fixtureName) { 
        def teams = fixtureName.split(" vs ") 
        def homeTeam = teams[0]?.trim() 
        def awayTeam = teams[1]?.trim() 
 
        // Access statistics data 
        def statistics = json.data?.statistics 
        if (statistics) { 
          def statsLines = [] 
 
          // Add header line to statsLines 
          def headerLine = [homeTeam, " TEAM STATS ", awayTeam] 
          statsLines << headerLine // Add statistics data to statsLines statistics.each { stat -> 
            if (stat.location == 'home') { 
              def typeId = stat.type_id 
              def name = stat.type?.name 
              def homeValue = stat.data?.value?.toString() 
              def awayStat = statistics.find { it.location == 'away' && it.type_id == typeId } 
              def awayValue = awayStat?.data?.value?.toString() ?: "N/A" 
 
              // Group home value, stat name, and away value together for alignment 
              def formattedLine = [homeValue, name, awayValue] 
              statsLines << formattedLine } } // Calculate image dimensions based on content int lineHeight = 40 // Space for each line of text int padding = 20 // Padding around the text int imgWidth = 800 // Fixed width for the image int imgHeight = (statsLines.size() * 2 + 2) * lineHeight + 2 * padding // Account for empty lines // Create the image for drawing text BufferedImage img = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB) Graphics g = img.createGraphics() // Set background to light grey (#F5F5F5) g.setColor(new Color(245, 245, 245)) // Light grey color (hex: #F5F5F5) g.fillRect(0, 0, imgWidth, imgHeight) // Set font color to black g.setColor(Color.BLACK) // Black text color g.setFont(new Font("Courier New", Font.BOLD, 20)) // Draw the header at the top int yPosition = padding + lineHeight // Starting position after padding // Draw statistics centered under the header yPosition += 2 * lineHeight // Start position after header and spacing statsLines.each { row -> 
            // Calculate the exact x position for each stat (home stat, stat name, away stat) 
            int homeStatX = (imgWidth / 4) - g.getFontMetrics().stringWidth(row[0]) / 2 
            int statNameX = (imgWidth / 2) - g.getFontMetrics().stringWidth(row[1]) / 2 
            int awayStatX = (imgWidth * 3 / 4) - g.getFontMetrics().stringWidth(row[2]) / 2 
 
            // Draw each stat under the header columns 
            g.drawString(row[0], homeStatX, yPosition) // Home stat column 
            g.drawString(row[1], statNameX, yPosition) // Stat name column 
            g.drawString(row[2], awayStatX, yPosition) // Away stat column 
 
            yPosition += 2 * lineHeight // Extra space for empty line between stats 
          } 
 
          // Save the image as a PNG file 
          File outputFile = new File("match_stats_image.png") 
          ImageIO.write(img, "PNG", outputFile) 
          println "Image has been saved as match_stats_image.png." 
        } else { 
          println "No statistics data found in the response." 
        } 
      } else { 
        println "Fixture name not found in the response." 
      } 
    } else { 
      println "Failed to fetch data. HTTP response code: ${connection.responseCode}" 
    } 
  } catch (Exception e) { 
    println "Error: ${e.message}" 
  } 
} 
 
// Helper function to draw centered text 
def drawCenteredString(Graphics g, String text, int y, int width) { 
  FontMetrics metrics = g.getFontMetrics() 
  int textWidth = metrics.stringWidth(text) 
  int startX = (width - textWidth) / 2 
  g.drawString(text, startX, y) 
} 
 
// API URL 
def apiUrl = "https://api.sportmonks.com/v3/football/fixtures/19134600?api_token=API_Token&include=statistics.type&filters=fixtureStatisticTypes:45,86,42,80,56" 
 
// Fetch and generate image 
fetchData(apiUrl)
Image has been saved as match_stats_image.png.

Explanation.

An image is created in memory with a specific width and height in the BufferedImage. The BufferedImage.TYPE_INT_ARGB specifies that the image will support transparency and RGB colour channels.

Graphics: This object provides the drawing tools to render text, shapes, or other graphics onto the image.

g.setColor(new Color(245, 245, 245)); sets the background colour to light grey using the Colour object. The fillRect method fills the entire image area with the chosen colour (starting at (0, 0) and covering the entire width and height).

Saving the Image: After drawing all the text, the image is saved as a PNG file to disc with the name match_stats_image.png. Ensure you have the necessary permission to save to the folder on your workstation.

The image is saved to the same folder where you have created your Groovy file.

Chapter 4: Making Our Code Dynamic.

In this chapter, we shall modify our code to print an image for any fixture of your choice.

We will do so by including the line: def fixtureId = System.console()?.readLine() ?: new Scanner(System.in).nextLine() which will force us to provide a fixture ID at runtime.

We will make our URL dynamic to accept any ID of choice by modifying our URL.

def apiUrl = “https://api.sportmonks.com/v3/football/fixtures/${fixtured}?api_token=…..”

import groovy.json.JsonSlurper 
import java.awt.Color 
import java.awt.Font 
import java.awt.Graphics 
import java.awt.image.BufferedImage 
import javax.imageio.ImageIO 
 
def fetchData(apiUrl) { 
  try { 
    // Open the URL connection 
    def url = new URL(apiUrl) 
    HttpURLConnection connection = (HttpURLConnection) url.openConnection() 
    connection.setRequestMethod("GET") 
    connection.setConnectTimeout(5000) 
    connection.setReadTimeout(5000) 
 
    // Check for a successful response 
    if (connection.responseCode == 200) { 
      // Parse the JSON response 
      def response = connection.inputStream.text 
      def json = new JsonSlurper().parseText(response) 
 
      // Access the fixture data (home and away team) 
      def fixtureName = json.data?.name 
      if (fixtureName) { 
        def teams = fixtureName.split(" vs ") 
        def homeTeam = teams[0]?.trim() 
        def awayTeam = teams[1]?.trim() 
 
        // Access statistics data 
        def statistics = json.data?.statistics 
        if (statistics) { 
          def statsLines = [] 
 
          // Add header line to statsLines 
          def headerLine = [homeTeam, " TEAM STATS ", awayTeam] 
          statsLines << headerLine // Add statistics data to statsLines statistics.each { stat -> 
            if (stat.location == 'home') { 
              def typeId = stat.type_id 
              def name = stat.type?.name 
              def homeValue = stat.data?.value?.toString() 
              def awayStat = statistics.find { it.location == 'away' && it.type_id == typeId } 
              def awayValue = awayStat?.data?.value?.toString() ?: "N/A" 
 
              // Group home value, stat name, and away value together for alignment 
              def formattedLine = [homeValue, name, awayValue] 
              statsLines << formattedLine } } // Calculate image dimensions based on content int lineHeight = 40 // Space for each line of text int padding = 20 // Padding around the text int imgWidth = 800 // Fixed width for the image int imgHeight = (statsLines.size() * 2 + 2) * lineHeight + 2 * padding // Account for empty lines // Create the image for drawing text BufferedImage img = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB) Graphics g = img.createGraphics() // Set background to light grey g.setColor(new Color(245, 245, 245)) // Light grey color g.fillRect(0, 0, imgWidth, imgHeight) // Set font color and style g.setColor(Color.BLACK) g.setFont(new Font("Courier New", Font.BOLD, 20)) // Draw the statistics int yPosition = padding + lineHeight statsLines.each { row -> 
            int homeStatX = (imgWidth / 4) - g.getFontMetrics().stringWidth(row[0]) / 2 
            int statNameX = (imgWidth / 2) - g.getFontMetrics().stringWidth(row[1]) / 2 
            int awayStatX = (imgWidth * 3 / 4) - g.getFontMetrics().stringWidth(row[2]) / 2 
 
            g.drawString(row[0], homeStatX, yPosition) 
            g.drawString(row[1], statNameX, yPosition) 
            g.drawString(row[2], awayStatX, yPosition) 
 
            yPosition += 2 * lineHeight 
          } 
 
          // Save the image 
          File outputFile = new File("match_stats_image.png") 
          ImageIO.write(img, "PNG", outputFile) 
          println "Image has been saved as match_stats_image.png." 
        } else { 
          println "No statistics data found in the response." 
        } 
      } else { 
        println "Fixture name not found in the response." 
      } 
    } else { 
      println "Failed to fetch data. HTTP response code: ${connection.responseCode}" 
    } 
  } catch (Exception e) { 
    println "Error: ${e.message}" 
  } 
} 
 
// Prompt the user for the fixture ID 
println "Please enter the fixture ID:" 
def fixtureId = System.console()?.readLine() ?: new Scanner(System.in).nextLine() 
 
// API URL 
def apiUrl = "https://api.sportmonks.com/v3/football/fixtures/${fixtureId}?api_token=API_Token&include=statistics.type&filters=fixtureStatisticTypes:45,86,42,80,56" 
 
// Fetch and generate image 
fetchData(apiUrl) 
 
Please enter the fixture ID:
19154581
Image has been saved as match_stats_image.png.

Using the fixture ID for the game between Bayern Munich and Bayer Leverkusen in the Bundesliga, 19154581, we can print out an image with the match stats just like we did for Manchester City vs. Tottenham.

Go ahead and give it a try with any other fixture ID from your current plan.

CHAPTER 5: Adding Your Logo.

In this chapter, our final, we shall add a logo to the generated image so it appears branded. Just like you might have guessed, we shall use our company’s logo, Sportmonks. However, there will be no need to let you know that you are at liberty to replace this with yours.

// Import statements remain unchanged
import groovy.json.JsonSlurper
import java.awt.Color
import java.awt.Font
import java.awt.Graphics
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
import java.io.File

def fetchData(apiUrl) {
    try {
        // Open the URL connection
        def url = new URL(apiUrl)
        HttpURLConnection connection = (HttpURLConnection) url.openConnection()
        connection.setRequestMethod("GET")
        connection.setConnectTimeout(5000)
        connection.setReadTimeout(5000)

        // Check for a successful response
        if (connection.responseCode == 200) {
            // Parse the JSON response
            def response = connection.inputStream.text
            def json = new JsonSlurper().parseText(response)

            // Access the fixture data (home and away team)
            def fixtureName = json.data?.name
            if (fixtureName) {
                def teams = fixtureName.split(" vs ")
                def homeTeam = teams[0]?.trim()
                def awayTeam = teams[1]?.trim()

                // Access statistics data
                def statistics = json.data?.statistics
                if (statistics) {
                    def statsLines = []

                    // Add header line to statsLines
                    def headerLine = [homeTeam, " TEAM STATS ", awayTeam]
                    statsLines << headerLine // Add statistics data to statsLines statistics.each { stat ->
                        if (stat.location == 'home') {
                            def typeId = stat.type_id
                            def name = stat.type?.name
                            def homeValue = stat.data?.value?.toString()
                            def awayStat = statistics.find { it.location == 'away' && it.type_id == typeId }
                            def awayValue = awayStat?.data?.value?.toString() ?: "N/A"

                            // Group home value, stat name, and away value together for alignment
                            def formattedLine = [homeValue, name, awayValue]
                            statsLines << formattedLine } } // Calculate image dimensions based on content int lineHeight = 40 // Space for each line of text int padding = 20 // Padding around the text int gapAfterLogo = 60 // Reduced gap between the logo and the content int imgWidth = 720 // Fixed width for the image int imgHeight = (statsLines.size() * 2 + 4) * lineHeight + 2 * padding + gapAfterLogo // Adjust height // Create the image for drawing text BufferedImage img = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB) Graphics g = img.createGraphics() // Set background to light grey g.setColor(new Color(245, 245, 245)) // Light grey color g.fillRect(0, 0, imgWidth, imgHeight) // Set font color and style g.setColor(Color.BLACK) g.setFont(new Font("Courier New", Font.BOLD, 20)) // Load and draw the logo with aspect ratio preserved int logoHeight = 200 // Maximum height for the logo int logoWidth = 200 // Maximum width for the logo int actualLogoHeight = 0 // Store the actual logo height try { File logoFile = new File("logo.png") // Assume logo.png is in the same directory if (logoFile.exists()) { BufferedImage logo = ImageIO.read(logoFile) int originalLogoWidth = logo.getWidth() int originalLogoHeight = logo.getHeight() // Scale logo to fit within the defined width and height, maintaining aspect ratio double scaleFactor = Math.min(logoWidth / (double) originalLogoWidth, logoHeight / (double) originalLogoHeight) int scaledLogoWidth = (int) (originalLogoWidth * scaleFactor) actualLogoHeight = (int) (originalLogoHeight * scaleFactor) // Center the logo horizontally int logoX = (imgWidth / 2) - (scaledLogoWidth / 2) int logoY = padding // Place the logo at the top g.drawImage(logo, logoX, logoY, scaledLogoWidth, actualLogoHeight, null) } else { println "Logo file not found in the current directory. Skipping logo." } } catch (Exception e) { println "Error loading logo: ${e.message}" } // Draw the statistics int yPosition = padding + actualLogoHeight + gapAfterLogo // Start right below the logo with reduced gap statsLines.each { row ->
                        int homeStatX = (imgWidth / 4) - g.getFontMetrics().stringWidth(row[0]) / 2
                        int statNameX = (imgWidth / 2) - g.getFontMetrics().stringWidth(row[1]) / 2
                        int awayStatX = (imgWidth * 3 / 4) - g.getFontMetrics().stringWidth(row[2]) / 2

                        g.drawString(row[0], homeStatX, yPosition)
                        g.drawString(row[1], statNameX, yPosition)
                        g.drawString(row[2], awayStatX, yPosition)

                        yPosition += 2 * lineHeight
                    }

                    // Save the image
                    File outputFile = new File("match_stats_image.png")
                    ImageIO.write(img, "PNG", outputFile)
                    println "Image has been saved as match_stats_image.png."
                } else {
                    println "No statistics data found in the response."
                }
            } else {
                println "Fixture name not found in the response."
            }
        } else {
            println "Failed to fetch data. HTTP response code: ${connection.responseCode}"
        }
    } catch (Exception e) {
        println "Error: ${e.message}"
    }
}

// Prompt the user for the fixture ID
println "Please enter the fixture ID:"
def fixtureId = System.console()?.readLine() ?: new Scanner(System.in).nextLine()

// API URL
def apiUrl = "https://api.sportmonks.com/v3/football/fixtures/${fixtureId}?api_token=API_Token&include=statistics.type&filters=fixtureStatisticTypes:45,86,42,80,56"

// Fetch and generate image
fetchData(apiUrl)
Please enter the fixture ID:
19154581
Image has been saved as match_stats_image.png.

In our new piece of code, we defined the dimensions by setting a maximum width and height (200×200) for the logo and initialised actualLogoHeight to track its scaled height. We then loaded the logo named ‘logo.png’ from the current directory, ensuring it exists.

We scaled it proportionally and positioned it before drawing the logo on our image. We also made slight adjustments to ensure the content is spaced moderately.

CONCLUSION.

Do you think you can make further improvements to the piece of code? How about adding club logos to replace the club names? If you think you are ready for the challenge, don’t hesitate to give it a try.

There’s plenty more you can do with Sportmonks Football API. Why don’t you take a look at our official documentation to know more?

FAQ

Are there any limitations or restrictions when using Sportmonks Football API with Postman?
Sportmonks Football API does not impose any specific limitations or restrictions on using Postman with the API. However, you should be mindful of your API usage limits based on your subscription plan. Additionally, ensure that you handle sensitive information such as API keys securely within Postman to prevent unauthorized access or misuse.
Can I customise the data retrieved from Sportmonks Football API?
Yes, you can customise the data retrieved from the Football API to suit your specific requirements. The API allows you to filter data based on various parameters, such as date, team, player, league, and more. Additionally, you can specify which fields you want to include or exclude in the API responses to minimize bandwidth usage and optimize performance. Check our documentation for more information.
How can I test the Sportmonks API for FREE?
You have two options:
  1. Create an account on My Sportmonks and get immediate access to our free plan.
  2. Subscribe to one of our paid plans and receive a one-time-only 14-day free trial.
How does the API work?
We have built a well-structured, flexible, and fast JSON API. Our API makes it easy to customize your JSON responses. Our API's flexibility demonstrates itself by allowing its users to build their own responses in the form of adding include parameters to their requests. This means you can request the data you need and leave out the data you don't need. Filtering and Sorting is another widely used and loved characteristic of our API. It means you can request the data the way you want it to, by adding the proper filtering and sorting commands. Please check our documentation pages for more information.
Is there a limit to the number of API requests I can make?
We offers different subscription plans with varying levels of access and usage limits. While the Free plan has certain restrictions on the number of API requests allowed per day, higher-tier plans offer increased usage (3,000 API calls per entity per hour) allowances and additional features. If you require more API requests than your current plan allows, you can consider upgrading to a higher-tier plan or contacting support for customised solutions tailored to your needs.

Written by Wesley Van Rooij

Wesley van Rooij is a marketing and football expert with over 5 years of industry experience. His comprehensive knowledge of the Sportmonks Football API and a focused approach to APIs in the Sports Data industry allow him to offer insights and support to enthusiasts and businesses. His outstanding marketing and communication skills and technical writing expertise enable him to empathise with developers. He understands their needs and challenges to facilitate the development of cutting-edge football applications that stand out in the market.

What’s next?

Transform your football app with more than 65 new stats. Sign up to unlock your app’s full potential.

Try for free