Initial commit

This commit is contained in:
Jan-Lukas Else 2023-06-24 16:12:50 +02:00
commit 4cd8197132
5 changed files with 387 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Jan-Lukas Else
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

76
README.md Normal file
View File

@ -0,0 +1,76 @@
# Cell Location Map using Mozilla Location Service
This repository provides code to display the cell locations using the Mozilla Location Service. The code extracts cell data from a CSV export file and generates an interactive map with markers representing the cell locations.
## Prerequisites
- Go programming language
- Access to a [cell export file from the Mozilla Location Service](https://location.services.mozilla.com/downloads)
## Installation
1. Clone the repository:
```shell
git clone https://git.jlel.se/jlelse/mls.git
```
2. Change to the project directory:
```shell
cd mls
```
## Usage
### Step 1: Filter Cell Data
To filter the cell data and generate an output file, run the following command:
```shell
go run ./filter/main.go <input CSV file> <output CSV file> <radio> <mcc> <network> <min samples> <output columns>
```
- `<input CSV file>`: Path to the input CSV file containing the cell data.
- `<output CSV file>`: Path to the output CSV file where the filtered data will be saved.
- `<radio>`: The desired radio type (e.g., LTE).
- `<mcc>`: The desired Mobile Country Code (e.g., 262 for Germany).
- `<network>`: The desired network code (e.g., 3 for o2-de).
- `<min samples>`: The minimum number of samples required for a cell to be included.
- `<output columns>`: Space-separated list of columns to include in the output.
Example:
```shell
go run ./filter/main.go ./MLS-full-cell-export.csv ./output.csv LTE 262 3 100 lon lat created updated cell
```
### Step 2: Generate Map
To generate an HTML map with the filtered cell data, run the following command:
```shell
go run ./web/main.go <input CSV file> <output HTML file>
```
- `<input CSV file>`: Path to the input CSV file containing the filtered cell data with the columns lon,lat,created,updated,cell.
- `<output HTML file>`: Path to the output HTML file where the map will be saved.
Example:
```shell
go run ./web/main.go ./output.csv ./map.html
```
## Acknowledgments
The code in this repository is based on the [Mozilla Location Service](https://location.services.mozilla.com/) and leverages the following open-source libraries:
- [Leaflet](https://leafletjs.com/) (version 1.9.4): An open-source JavaScript library for interactive maps.
- [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster) (version 1.4.1): A plugin for Leaflet that provides clustering functionality for markers.
The idea for this project was inspired by the [CellInspector Map](https://mls.maxomagier.de/map.html).
## License
This project is licensed under the [MIT License](LICENSE).

126
filter/main.go Normal file
View File

@ -0,0 +1,126 @@
package main
import (
"encoding/csv"
"fmt"
"io"
"os"
)
func main() {
// Check if the correct number of command-line arguments is provided
if len(os.Args) < 6 {
fmt.Println("Usage: go run main.go <input CSV file> <output CSV file> <radio> <mcc> <network> <min samples> <output columns>")
return
}
// Get the command-line arguments
inputFile := os.Args[1]
outputFile := os.Args[2]
desiredRadio := os.Args[3]
desiredMCC := os.Args[4]
desiredNetwork := os.Args[5]
desiredMinSamples := os.Args[6]
desiredColumns := os.Args[7:]
// Open the input CSV file
input, err := os.Open(inputFile)
if err != nil {
fmt.Println("Error opening the input file:", err)
return
}
defer input.Close()
// Create the output CSV file
output, err := os.Create(outputFile)
if err != nil {
fmt.Println("Error creating the output file:", err)
return
}
defer output.Close()
// CSV reader and writer initialization
reader := csv.NewReader(input)
writer := csv.NewWriter(output)
// Read the header row to get the column indices
header, err := reader.Read()
if err != nil {
fmt.Println("Error reading CSV header:", err)
return
}
radioIndex := findColumnIndex(header, "radio")
mccIndex := findColumnIndex(header, "mcc")
netIndex := findColumnIndex(header, "net")
samplesIndex := findColumnIndex(header, "samples")
// Find the indices of the desired columns
desiredIndices := make([]int, len(desiredColumns))
for i, column := range desiredColumns {
desiredIndices[i] = findColumnIndex(header, column)
if desiredIndices[i] == -1 {
fmt.Println("Column not found:", column)
return
}
}
// Write the header row to the output file
err = writer.Write(desiredColumns)
if err != nil {
fmt.Println("Error writing the header row:", err)
return
}
// Read the CSV data line by line
for {
row, err := reader.Read()
if err == io.EOF {
break
} else if err != nil {
fmt.Println("Error reading CSV data:", err)
return
}
// Check the filter criteria
radio := row[radioIndex]
mcc := row[mccIndex]
net := row[netIndex]
samples := row[samplesIndex]
if radio == desiredRadio && mcc == desiredMCC && net == desiredNetwork && samples >= desiredMinSamples {
// Extract the desired columns based on indices
filteredRow := make([]string, len(desiredIndices))
for i, index := range desiredIndices {
filteredRow[i] = row[index]
}
// Write the row to the output file
err := writer.Write(filteredRow)
if err != nil {
fmt.Println("Error writing the row:", err)
return
}
}
}
// Write CSV writer data to the file
writer.Flush()
if err := writer.Error(); err != nil {
fmt.Println("Error writing the output file:", err)
return
}
fmt.Println("Filtering completed.")
}
// Helper function to find the index of a column name in the header row
func findColumnIndex(header []string, columnName string) int {
for i, name := range header {
if name == columnName {
return i
}
}
return -1
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.jlel.se/jlelse/mls
go 1.20

161
web/main.go Normal file
View File

@ -0,0 +1,161 @@
package main
import (
"encoding/csv"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
)
const (
leafletVersion = "1.9.4"
markerClusterVersion = "1.4.1"
htmlTemplateStart = `<!DOCTYPE html>
<html>
<head>
<title>Location Map</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@%s/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@%s/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@%s/dist/MarkerCluster.Default.css" />
<style>
#map {
height: 800px;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="https://cdn.jsdelivr.net/npm/leaflet@%s/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster@%s/dist/leaflet.markercluster.js"></script>
<script>
var map = L.map('map').setView([0, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors',
maxZoom: 18,
}).addTo(map);
var markers = L.markerClusterGroup();
`
htmlTemplateEnd = `
map.addLayer(markers);
</script>
</body>
</html>`
)
func main() {
if len(os.Args) != 3 {
fmt.Println("Usage: go run main.go <input CSV file (needs lon,lat,created,updated,cell)> <output HTML file>")
return
}
inputFile := os.Args[1]
outputFile := os.Args[2]
// Open the CSV file
file, err := os.Open(inputFile)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Read the CSV records
reader := csv.NewReader(file)
records, err := readCSVRecords(reader)
if err != nil {
log.Fatal(err)
}
// Generate the HTML
html := generateHTML(records)
// Save the HTML to a file
err = writeHTMLToFile(outputFile, html)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Map generated successfully and saved to %s.\n", outputFile)
}
func readCSVRecords(reader *csv.Reader) ([][]string, error) {
reader.Comma = ',' // Set the delimiter to comma explicitly
records, err := reader.ReadAll()
if err != nil {
return nil, err
}
// Remove the header row
if len(records) > 0 {
records = records[1:]
}
return records, nil
}
func generateHTML(records [][]string) string {
var htmlBuilder strings.Builder
htmlBuilder.WriteString(fmt.Sprintf(htmlTemplateStart, leafletVersion, markerClusterVersion, markerClusterVersion, leafletVersion, markerClusterVersion))
for _, record := range records {
// Parse longitude and latitude
lon, err := strconv.ParseFloat(record[0], 64)
if err != nil {
log.Fatalf("Error parsing longitude: %v\nLongitude value: %s\n", err, record[0])
}
lat, err := strconv.ParseFloat(record[1], 64)
if err != nil {
log.Fatalf("Error parsing latitude: %v\nLatitude value: %s\n", err, record[1])
}
// Get created and updated timestamps
created := record[2]
updated := record[3]
cell := record[4]
htmlBuilder.WriteString(fmt.Sprintf(`
var marker = L.marker([%f, %f]);
marker.bindTooltip("Created: %s<br />Updated: %s<br />Cell: %s");
markers.addLayer(marker);
`, lat, lon, formatTimestamp(created), formatTimestamp(updated), cell))
}
htmlBuilder.WriteString(htmlTemplateEnd)
return htmlBuilder.String()
}
func formatTimestamp(timestamp string) string {
// Parse the Unix timestamp string to an integer
unixTime, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return "" // Return an empty string if parsing fails
}
// Convert the Unix timestamp to a time.Time value
timeValue := time.Unix(unixTime, 0)
// Format the time
isoFormatted := timeValue.Format(time.RFC3339)
return isoFormatted
}
func writeHTMLToFile(filename string, html string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(html)
if err != nil {
return err
}
return nil
}