Add oramap

This commit is contained in:
dvirlabs 2025-05-27 23:29:02 +03:00
parent 2334ec08a2
commit c410843f5f
18 changed files with 470 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
package-lock.json
package.json

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]

6
README copy.md Normal file
View File

@ -0,0 +1,6 @@
# How to Run
```
npm init -y
npm install express
node server.js
```

10
data/families.json Normal file
View File

@ -0,0 +1,10 @@
[
{ "family": "Kafe (קאפח)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
{ "family": "Shiheb (שחב-שבח)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Manakhah (מנאכה)", "lat": 15.3019, "lng": 43.5983 },
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Dhamar (ד'מאר)", "lat": 14.5424, "lng": 44.4056 },
{ "family": "Salumi (סלומי-שלומי)", "city": "Al Kafla (אל קפלה)", "lat": 16.0240, "lng": 43.9790 },
{ "family": "Afgin (עפג'ין)", "city": "Sa'dah (צעדה)", "lat": 16.9402, "lng": 43.7639 },
{ "family": "Eraki (עראקי)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 }
]

23
oramap/.helmignore Normal file
View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

24
oramap/Chart.yaml Normal file
View File

@ -0,0 +1,24 @@
apiVersion: v2
name: oramap
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

View File

@ -0,0 +1,11 @@
{{- define "oramap.name" -}}
{{ .Chart.Name }}
{{- end }}
{{- define "oramap.fullname" -}}
{{ include "oramap.name" . }}-{{ .Release.Name }}
{{- end }}
{{- define "oramap.chart" -}}
{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
{{- end }}

View File

@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "oramap.fullname" . }}
labels:
app: {{ include "oramap.name" . }}
chart: {{ include "oramap.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "oramap.name" . }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ include "oramap.name" . }}
release: {{ .Release.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.containerPort }}
name: http
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}

View File

@ -0,0 +1,33 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "oramap.fullname" . }}
annotations:
{{- if .Values.ingress.className }}
kubernetes.io/ingress.class: {{ .Values.ingress.className }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "oramap.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "oramap.fullname" . }}
labels:
app: {{ include "oramap.name" . }}
chart: {{ include "oramap.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.containerPort }}
protocol: TCP
name: http
selector:
app: {{ include "oramap.name" . }}
release: {{ .Release.Name }}

22
oramap/values.yaml Normal file
View File

@ -0,0 +1,22 @@
replicaCount: 1
image:
repository: harbor.dvirlabs.com/shay/oramap
tag: "1"
pullPolicy: IfNotPresent
containerPort: 3000
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: "traefik"
hosts:
- host: oramap.dvirlabs.com
paths:
- path: /
pathType: Prefix
tls: []

26
public/index.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>🗺️ Ora</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
</head>
<body>
<header>
<h1>🗺️ Ora</h1>
<div class="search-bar">
<input type="text" id="searchInput" placeholder="Enter family name..." />
<button onclick="searchFamily()">Search</button>
</div>
</header>
<main>
<div id="map"></div>
</main>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.js"></script>
<script src="script.js"></script>
</body>
</html>

BIN
public/logo-shay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

124
public/script.js Normal file
View File

@ -0,0 +1,124 @@
let map = L.map('map').setView([15.5527, 48.5164], 6);
// Define the two tile layers
const voyager = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap contributors &copy; CARTO',
subdomains: 'abcd',
maxZoom: 19
});
const openStreetMap = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data © OpenStreetMap contributors',
maxZoom: 19
});
// Add the default layer (Voyager)
voyager.addTo(map);
// Group the layers for switching
const baseMaps = {
"English Map (CartoDB Voyager)": voyager,
"Original Map (OpenStreetMap)": openStreetMap
};
// Add the layer control to the map
L.control.layers(baseMaps).addTo(map);
async function searchFamily() {
const familyName = document.getElementById('searchInput').value.trim();
if (!familyName) {
alert('Please enter a family name.');
return;
}
try {
const response = await fetch(`/search?family=${encodeURIComponent(familyName)}`);
const familyResult = await response.json();
if (!familyResult.length) {
alert('No matching family found.');
return;
}
clearMarkers();
familyResult.forEach(record => {
L.marker([record.lat, record.lng]).addTo(map)
.bindPopup(`<strong>${record.family}</strong><br>City: ${record.city}`)
.openPopup();
});
const first = familyResult[0];
map.setView([first.lat, first.lng], 7);
} catch (error) {
console.error('Search error:', error);
alert('Something went wrong while searching. Please try again later.');
}
}
function clearMarkers() {
map.eachLayer(layer => {
if (layer instanceof L.Marker) {
map.removeLayer(layer);
}
});
}
const familyNames = [
"Kafe (קאפח)", "Shiheb (שחב-שבח)", "Eraki (עראקי)", "Salumi (סלומי-שלומי)",
"Afgin (עפג'ין)", "Uzeyri (עזירי-עוזרי)"
// Add more families here if you like
];
// Initialize Fuse.js
const fuse = new Fuse(familyNames, {
includeScore: true,
threshold: 0.4 // Lower = stricter matching
});
const searchInput = document.getElementById('searchInput');
const suggestionsBox = document.createElement('div');
suggestionsBox.classList.add('suggestions');
searchInput.parentNode.style.position = 'relative'; // Make parent relative
searchInput.parentNode.appendChild(suggestionsBox);
searchInput.addEventListener('input', () => {
const value = searchInput.value.trim();
suggestionsBox.innerHTML = '';
if (!value) return;
const results = fuse.search(value);
results.forEach(result => {
const option = document.createElement('div');
option.textContent = result.item;
option.onclick = () => {
searchInput.value = result.item;
suggestionsBox.innerHTML = '';
};
suggestionsBox.appendChild(option);
});
});
L.control.logo = function (opts) {
return new L.Control.Logo(opts);
};
L.Control.Logo = L.Control.extend({
onAdd: function () {
const div = L.DomUtil.create('div', 'custom-logo');
div.innerHTML = `
<img src="logo.png" alt="Logo" style="height: 40px; vertical-align: middle;">
<span style="margin-left: 8px; font-weight: bold; color: #333;">Shevach</span>
`;
return div;
},
onRemove: function () {
// Nothing to clean up
}
});
L.control.logo({ position: 'bottomleft' }).addTo(map);

98
public/style.css Normal file
View File

@ -0,0 +1,98 @@
/* Reset some basic styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f4f4f4;
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Header styles */
header {
background-color: #2c3e50;
padding: 20px;
text-align: center;
color: white;
}
h1 {
margin-bottom: 10px;
}
.search-bar {
margin-top: 10px;
}
.search-bar input {
padding: 10px;
width: 250px;
border: none;
border-radius: 5px;
margin-right: 8px;
}
.search-bar button {
padding: 10px 15px;
border: none;
border-radius: 5px;
background-color: #3498db;
color: white;
cursor: pointer;
font-weight: bold;
}
.suggestions {
background: #363251;
border: 1px solid #ccc;
position: absolute;
top: 110%; /* Just below the input */
left: 48%;
transform: translateX(-50%);
max-width: fit-content;
min-width: 200px; /* Optional: minimum size */
max-height: 200px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
transition: all 0.2s ease-in-out;
}
.suggestions div {
padding: 10px;
cursor: pointer;
}
.suggestions div:hover {
background-color: #95a1a8;
}
.custom-logo {
background: rgba(255, 255, 255, 0.9);
padding: 6px 10px;
border-radius: 8px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
font-family: sans-serif;
}
/* Map container */
#map {
flex-grow: 1;
height: 80vh;
width: 100%;
}
/* Main container */
main {
flex: 1;
display: flex;
flex-direction: column;
}

3
run_ora_map.sh Normal file
View File

@ -0,0 +1,3 @@
npm init -y
npm install express
node server.js

19
server.js Normal file
View File

@ -0,0 +1,19 @@
const express = require('express');
const app = express();
const families = require('./data/families.json');
app.use(express.static('public'));
app.get('/search', (req, res) => {
const query = req.query.family?.toLowerCase();
if (!query) {
return res.json([]);
}
const matches = families.filter(fam => fam.family.toLowerCase().includes(query));
res.json(matches);
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});