├── public ├── favicon.ico ├── summary_screenshot.png ├── panel_packing_screenshot.png ├── search_feature_screenshot.png └── vercel.svg ├── .eslintrc.json ├── next-env.d.ts ├── pages ├── api │ └── hello.ts ├── _app.tsx └── index.tsx ├── next.config.js ├── .gitignore ├── tsconfig.json ├── package.json ├── components ├── places.tsx └── map.tsx ├── README.md └── styles ├── Home.module.css └── globals.css /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushagrachopra18/Solar-Roof-Measurement-Calaculator/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/summary_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushagrachopra18/Solar-Roof-Measurement-Calaculator/HEAD/public/summary_screenshot.png -------------------------------------------------------------------------------- /public/panel_packing_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushagrachopra18/Solar-Roof-Measurement-Calaculator/HEAD/public/panel_packing_screenshot.png -------------------------------------------------------------------------------- /public/search_feature_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushagrachopra18/Solar-Roof-Measurement-Calaculator/HEAD/public/search_feature_screenshot.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next", 3 | "rules": { 4 | "react/no-unescaped-entities": "off", 5 | "@next/next/no-page-custom-font": "off" 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | typescript: { 5 | // !! WARN !! 6 | // Dangerously allow production builds to successfully complete even if 7 | // your project has type errors. 8 | // !! WARN !! 9 | ignoreBuildErrors: true, 10 | }, 11 | } 12 | 13 | module.exports = nextConfig 14 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | import Head from "next/head"; 3 | import "../styles/globals.css"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | 9 | Solar Roof Measurement Calculator 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default MyApp 17 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLoadScript } from "@react-google-maps/api"; 2 | import Map from "../components/map"; 3 | 4 | function Home() { 5 | 6 | const { isLoaded } = useLoadScript ({ 7 | googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY as string, 8 | libraries: ["places", "geometry"] 9 | }); 10 | 11 | if (!isLoaded) return
Loading...
12 | return ; 13 | } 14 | 15 | export default Home 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roof-measurement-calaculator", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@reach/combobox": "^0.17.0", 13 | "@react-google-maps/api": "^2.10.2", 14 | "next": "12.1.6", 15 | "react": "18.1.0", 16 | "react-dom": "18.1.0", 17 | "use-places-autocomplete": "^2.0.0" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "17.0.31", 21 | "@types/react": "18.0.8", 22 | "@types/react-dom": "18.0.3", 23 | "eslint": "8.14.0", 24 | "eslint-config-next": "12.1.6", 25 | "typescript": "4.6.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /components/places.tsx: -------------------------------------------------------------------------------- 1 | import usePlacesAutocomplete, { 2 | getGeocode, 3 | getLatLng, 4 | } from "use-places-autocomplete"; 5 | import { 6 | Combobox, 7 | ComboboxInput, 8 | ComboboxPopover, 9 | ComboboxList, 10 | ComboboxOption, 11 | } from "@reach/combobox"; 12 | import "@reach/combobox/styles.css"; 13 | 14 | type PlacesProps = { 15 | setHome: (position: google.maps.LatLngLiteral) => void; 16 | }; 17 | 18 | export default function Places({ setHome }: PlacesProps) { 19 | const {ready, value, setValue, suggestions: {status, data}, clearSuggestions} = usePlacesAutocomplete(); 20 | 21 | const handleSelect = async (val: string) => { 22 | setValue(val, false); 23 | clearSuggestions(); 24 | 25 | const results = await getGeocode({address: val}); 26 | const {lat, lng} = await getLatLng(results[0]); 27 | setHome({lat, lng}); 28 | }; 29 | 30 | return( 31 | 32 |
36 |

🔍

37 | { 40 | setValue(e.target.value) 41 | }} 42 | disabled={!ready} 43 | className="combobox-input" 44 | placeholder="Search home address" 45 | style={{ 46 | 'width': '100%', 47 | 'margin': '10px', 48 | 'borderRadius': '2px' 49 | }} 50 | /> 51 |
52 | 53 | 54 | {status === "OK" && data.map(({place_id, description}) => ( 55 | 59 | ))} 60 | 61 | 62 |
63 | ); 64 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solar Roof Measurement Calculator 2 | 3 | A tool to measure your roof and tell you how much of your home’s energy consumption you can offset with solar. Made with Next.JS in TypeScript and includes an extensive integration with Google Maps API. Includes a rectangle packing algorithm for triangular shaped roof panels. 4 | 5 | 6 | **App deployed here:** https://roof-measurement-calaculator-gieetdjrk-kushagrachopra18.vercel.app/ 7 | 8 | *Note:* If you are reading this **after August 2022**, it's likely that the deployed version of the app doesn't work anymore since my free trial to the Google Maps API has ended. Feel free to check out the video below to see how it's supposed to work! 9 | 10 | **Video demo of the app:** https://www.youtube.com/watch?v=3I8DnJDj9s4 11 | 12 | ## How to use 13 | *Also shown in video above* 14 | 15 | **Basic use** 16 | 1. Use the address search box to find your home 17 | 2. Highlight each south, east, and west facing panel of your roof by (for each panel): 18 | 1. Click all of the corners of the roof panel to outline it 19 | 2. Click the original point you placed to complete the highlight 20 | 3. Repeat for remaining panels 21 | 3. Enter your home's monthly energy consumption in the summary section 22 | 4. View summary to figure out how much of your home's energy consumption you can offset with solar panels, how many solar panels you can fit, and how much energy that will generate per month 23 | 24 | **Miscellaneous Features** 25 | - Delete panels by either: 26 | 1. Clicking on them 27 | 2. Clicking the delete button on the corresponding block in the "Drawn Panels" section 28 | - Add panels back by clicking the corresponding number in the "Click to add panel back:" section 29 | - Identify panel number by hovering over panel and looking towards the top of the screen for the "Currently hovering over" popup 30 | 31 | ## Screenshots 32 | 33 | *Solar panel packing algorithm* 34 | 35 | Getting started 36 | 37 | *Address search feature* 38 | 39 | Getting started 40 | 41 | *Summary section* 42 | 43 | Getting started 44 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | buntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | /* font-family: Arial, Helvetica, sans-serif; */ 8 | } 9 | 10 | h1{ 11 | color: orange; 12 | } 13 | 14 | a { 15 | color: inherit; 16 | text-decoration: none; 17 | } 18 | 19 | * { 20 | box-sizing: border-box; 21 | } 22 | 23 | .bold{ 24 | font-weight: bold; 25 | color: orange; 26 | } 27 | 28 | .container { 29 | display: flex; 30 | height: 100vh; 31 | } 32 | 33 | .controls { 34 | width: 20%; 35 | padding: 1rem; 36 | background: #14161a; 37 | color: #fff; 38 | overflow: auto; 39 | } 40 | 41 | .controls input { 42 | border: none; 43 | } 44 | 45 | .map-container { 46 | width: 100%; 47 | height: 100vh; 48 | } 49 | 50 | .hovering_over_message{ 51 | position: absolute; 52 | z-index: 90000; 53 | left: 45%; 54 | background-color: orange; 55 | opacity: .6; 56 | font-size: 20px; 57 | padding: 15px; 58 | margin: 20px 0; 59 | 60 | border-radius: 5px; 61 | } 62 | 63 | .map { 64 | width: 80%; 65 | height: 100vh; 66 | } 67 | 68 | .highlight { 69 | font-size: 1.25rem; 70 | font-weight: bold; 71 | } 72 | 73 | .combobox-input { 74 | width: 100%; 75 | padding: 0.5rem; 76 | } 77 | 78 | .roof_panel_info_box{ 79 | margin: 20px 0; 80 | padding: 5px 10px; 81 | /* border: 1px solid lightgray; */ 82 | background-color: rgba(236, 236, 236, .3); 83 | border-radius: 5px; 84 | 85 | display: flex; 86 | flex-direction: column; 87 | } 88 | 89 | .roof_panel_info_box h1{ 90 | font-size: 20px; 91 | margin-bottom: 10px; 92 | color: white; 93 | } 94 | 95 | .roof_panel_info_box p{ 96 | margin-top: 0; 97 | } 98 | 99 | .delete_button{ 100 | background-color: #F47174; 101 | 102 | font-size: 12px; 103 | border: none; 104 | padding: 5px 10px; 105 | border-radius: 5px; 106 | 107 | margin-bottom: 5px; 108 | 109 | color: white; 110 | } 111 | 112 | .deleted_panels_outer_container{ 113 | display: flex; 114 | flex-wrap: wrap; 115 | align-items: center; 116 | } 117 | 118 | .deleted_panels_description{ 119 | font-size: 10px; 120 | width: 30%; 121 | } 122 | 123 | .deleted_panels_inner_container{ 124 | background-color: rgba(236, 112, 113, .6); 125 | padding: 5px; 126 | width: 70%; 127 | display: flex; 128 | flex-wrap: wrap; 129 | align-items: center; 130 | border-radius: 5px; 131 | } 132 | 133 | .deleted_panels_button{ 134 | background-color: rgba(236, 112, 113, 1); 135 | border: none; 136 | color: white; 137 | margin: 3px 5px; 138 | padding: 3px 7px; 139 | border-radius: 5px; 140 | 141 | transition: .2s; 142 | } 143 | 144 | .deleted_panels_button:hover{ 145 | opacity: .6; 146 | transition: .2s; 147 | } 148 | -------------------------------------------------------------------------------- /components/map.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useCallback, useRef, useEffect } from "react"; 2 | import { 3 | GoogleMap, 4 | Marker, 5 | DirectionsRenderer, 6 | Circle, 7 | MarkerClusterer, 8 | Polyline, 9 | Polygon, 10 | DrawingManager, 11 | useGoogleMap, 12 | } from "@react-google-maps/api"; 13 | import Places from "./places"; 14 | import { unmountComponentAtNode } from "react-dom"; 15 | import React from "react"; 16 | // import Distance from "./distance"; 17 | 18 | type LatLngLiteral = google.maps.LatLngLiteral; 19 | type MapOptions = google.maps.MapOptions; 20 | 21 | export default function Map() { 22 | const [energyConsumption, setEnergyConsumption] = useState(900); //in kWh 23 | const solarPanelWidth: number = 3.5; //in ft 24 | const solarPanelHeight: number = 5; //in ft 25 | const solarPanelArea: number = solarPanelWidth * solarPanelHeight; //in sq ft 26 | const solarPanelEnergyOutput: number = 48; //in kWh 27 | 28 | 29 | const [zoom, setZoom] = useState(12); 30 | const [panelHovering, setPanelHovering] = useState(); 31 | 32 | const [home, setHome] = useState(); 33 | useEffect(() => { 34 | if(home){ 35 | setZoom(100); 36 | } 37 | }, [home]); 38 | 39 | const mapRef = useRef(); 40 | const center = useMemo(() => ({lat: 29.425319, lng: -98.492733}), []); 41 | 42 | const options = useMemo(() => ({ 43 | mapId: "793ef8405dde11b1", 44 | disableDefaultUI: true, 45 | clickableIcons: false, 46 | rotateControl: false, 47 | tilt: 0, 48 | mapTypeId: 'hybrid', 49 | draggableCursor: 'crosshair', 50 | }), []); 51 | const onLoad = useCallback((map: google.maps.Map) => (mapRef.current = map), []); 52 | 53 | const dotIcon = { 54 | url: "https://www.wsfcu.com/wp-content/uploads/Decorative-Orange-Box-Slider.jpg", 55 | scaledSize: new google.maps.Size(10, 10), // scaled size 56 | origin: new google.maps.Point(0,0), // origin 57 | anchor: new google.maps.Point(5, 5) // anchor 58 | }; 59 | 60 | //Handle creating and drawing the current Polyline 61 | 62 | const [boxPoints, setBoxPoints] = useState([]); 63 | const currPolyline = useRef(); 64 | 65 | let addBoxPoint = (coordinates: LatLngLiteral) => { 66 | setBoxPoints([...boxPoints, coordinates]); 67 | }; 68 | 69 | useEffect(() => { 70 | if(boxPoints.length >= 2){ 71 | if(currPolyline.current !== undefined){ 72 | currPolyline.current.setMap(null); 73 | } 74 | const newPolyline = new google.maps.Polyline({ 75 | path: boxPoints, 76 | geodesic: false, 77 | strokeColor: "#FF0000", 78 | }); 79 | newPolyline.setMap(mapRef.current!); 80 | // newPolyline.addListener('click', () => {newPolyline.setMap(null);}) 81 | currPolyline.current = newPolyline; 82 | } else { 83 | if(currPolyline.current !== undefined){ 84 | currPolyline.current.setMap(null); 85 | } 86 | } 87 | }); 88 | 89 | // Handle creating and drawing the panel Polygons 90 | 91 | const[roofPanels, setRoofPanels] = useState([]); 92 | const[deletedPanels, setDeletedPanels] = useState([]); 93 | 94 | enum CardinalDirection{ 95 | north, 96 | south, 97 | east, 98 | west 99 | } 100 | function drawPoint(points: LatLngLiteral | google.maps.LatLng){ 101 | const point = new google.maps.Marker({ 102 | position: points, 103 | icon: dotIcon 104 | }); 105 | point.setMap(mapRef.current!); 106 | } 107 | 108 | class roofPanel{ 109 | isDeleted: boolean = false; 110 | points: LatLngLiteral[]; 111 | panel: google.maps.Polygon; 112 | area: number; 113 | index: number; 114 | solarPanels: google.maps.Polygon[] = []; 115 | 116 | constructor(points: LatLngLiteral[], index: number){ 117 | const panel = new google.maps.Polygon({ 118 | paths: points, 119 | strokeColor: "#FF0000", 120 | strokeOpacity: 0.8, 121 | strokeWeight: 2, 122 | fillColor: "#FF0000", 123 | fillOpacity: 0.35, 124 | }); 125 | panel.setMap(mapRef.current!); 126 | panel.addListener('click', () => { 127 | this.delete(); 128 | }); 129 | panel.addListener('mouseover', () => {setPanelHovering(index);}); 130 | panel.addListener('mouseout', () => {setPanelHovering(undefined);}); 131 | 132 | this.area = google.maps.geometry.spherical.computeArea(points) * 10.7639; //convert square meters to sqaure feet 133 | this.index = index; 134 | this.points = points; 135 | this.panel = panel; 136 | 137 | //Draw solar panels 138 | switch (points.length) { 139 | case 3: //If triangle 140 | this.drawSolarPanelsInTriangle(points); 141 | default: 142 | break; 143 | } 144 | } 145 | 146 | private drawSolarPanelsInTriangle(points:LatLngLiteral[]): boolean{ 147 | //1. Identify side with largest vertical component---------------------------------- 148 | let yComponentLengths:number[][] = []; 149 | 150 | points.forEach((point, index) => { 151 | yComponentLengths.push([]); 152 | points.map((otherPoint) => { 153 | yComponentLengths[index].push(Math.abs(point.lat - otherPoint.lat)); 154 | }); 155 | }); 156 | 157 | let maxVal = 0; 158 | let maxLine: LatLngLiteral[] = [{lat:0, lng:0}, {lat:0, lng:0}]; 159 | 160 | for(let r = 0; r < yComponentLengths.length; r++){ 161 | for(let c = 0; c < yComponentLengths.length; c++){ 162 | if(yComponentLengths[r][c] > maxVal){ 163 | maxVal = yComponentLengths[r][c]; 164 | maxLine[0] = points[r]; 165 | maxLine[1] = points[c]; 166 | } 167 | } 168 | } 169 | 170 | //Draws blue line over side with largest y component (for debugging) 171 | // const maxLineDrawing = new google.maps.Polyline({ 172 | // path: maxLine, 173 | // geodesic: false, 174 | // strokeColor: "#0000FF", 175 | // }); 176 | // maxLineDrawing.setMap(mapRef.current!); 177 | 178 | //2. Identify nothmost point on line---------------------------------- 179 | let northMost: LatLngLiteral; 180 | let southMost: LatLngLiteral; 181 | if(maxLine[0].lat > maxLine[1].lat){ 182 | northMost = maxLine[0]; 183 | southMost = maxLine[1]; 184 | }else{ 185 | northMost = maxLine[1]; 186 | southMost = maxLine[0]; 187 | } 188 | 189 | //Draws a massive "north symbol" at the nothernmost point of the side with largest y component (for debugging) 190 | // const northPointDrawing = new google.maps.Marker({ 191 | // position: northMost, 192 | // icon: "data:image/png;base64,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" 193 | // }); 194 | // northPointDrawing.setMap(mapRef.current); 195 | 196 | // 3. Identify if rest of triangle is on left or right of line---------------------------------- 197 | 198 | //Draws a draws a "dotIcon" at the point that 1 solar panel height below the northmost point the side with largest y component (for debugging) 199 | // let temp = northMost; 200 | // temp.lat = temp.lat - (solarPanelHeight/3280.4/(10000/90)); 201 | // const onePanelHeightDown = new google.maps.Marker({ 202 | // position: temp, 203 | // icon: dotIcon 204 | // }); 205 | // onePanelHeightDown.setMap(mapRef.current); 206 | 207 | let maxLineHeading = google.maps.geometry.spherical.computeHeading(northMost, southMost); 208 | let solarPanelDistanceOnLine = Math.abs((solarPanelHeight * 0.3048)/(Math.cos(((180-Math.abs(maxLineHeading))*(Math.PI/180))))); //in meters 209 | let currPointOnMaxLine = google.maps.geometry.spherical.computeOffset(northMost, solarPanelDistanceOnLine, maxLineHeading); 210 | 211 | //Draws a "dotIcon" at the point on the side with largest y component at the point that corresponds to the latitude of "onePanelHeightDown" 212 | // *Note: This method is buggy, seems like due to some issues on Google Maps's side of things 213 | // -Tied: 214 | // -Deriving and using formula of line 215 | // -Using the interpolate function 216 | // const onePanelHeightDownOnLine = new google.maps.Marker({ 217 | // position: currPointOnMaxLine, 218 | // icon: dotIcon 219 | // }); 220 | // onePanelHeightDownOnLine.setMap(mapRef.current); 221 | 222 | let westOfCurrPointOnMaxLine = google.maps.geometry.spherical.computeOffset(currPointOnMaxLine, (solarPanelWidth * 0.3048), 270); 223 | let eastOfCurrPointOnMaxLine = google.maps.geometry.spherical.computeOffset(currPointOnMaxLine, (solarPanelWidth * 0.3048), 90); 224 | 225 | let canDrawPanels = true; 226 | if(google.maps.geometry.poly.containsLocation(westOfCurrPointOnMaxLine, this.panel)){ 227 | while(canDrawPanels){ 228 | this.drawRowOfPanels(currPointOnMaxLine, CardinalDirection.west); 229 | let southPointOfNextPanel = google.maps.geometry.spherical.computeOffset(currPointOnMaxLine, (2 * solarPanelHeight * 0.3048), 180); 230 | if(southPointOfNextPanel.lat() - southMost.lat < 0){ 231 | canDrawPanels = false; 232 | } 233 | currPointOnMaxLine = google.maps.geometry.spherical.computeOffset(currPointOnMaxLine, solarPanelDistanceOnLine, maxLineHeading); 234 | } 235 | }else if(google.maps.geometry.poly.containsLocation(eastOfCurrPointOnMaxLine, this.panel)){ 236 | while(canDrawPanels){ 237 | this.drawRowOfPanels(currPointOnMaxLine, CardinalDirection.east); 238 | let southPointOfNextPanel = google.maps.geometry.spherical.computeOffset(currPointOnMaxLine, (2 * solarPanelHeight * 0.3048), 180); 239 | if(southPointOfNextPanel.lat() - southMost.lat < 0){ 240 | canDrawPanels = false; 241 | } 242 | currPointOnMaxLine = google.maps.geometry.spherical.computeOffset(currPointOnMaxLine, solarPanelDistanceOnLine, maxLineHeading); 243 | } 244 | } 245 | 246 | //4. Draw as many solar panels as possible to the left or right---------------------------------- 247 | 248 | //5. Go down 1 row and repeat Step 4 if you are not below the latitude of the southmost point of the longest side, if you can't then terminate---------------------------------- 249 | 250 | return true; 251 | } 252 | 253 | private getLngOnLine(startPoint: LatLngLiteral, endPoint: LatLngLiteral, targetLat: number): number{ 254 | return (targetLat - startPoint.lat)*((endPoint.lng-startPoint.lng)/(endPoint.lat-startPoint.lat))+startPoint.lng; 255 | } 256 | 257 | private drawRowOfPanels(origin:LatLngLiteral|google.maps.LatLng, direction:CardinalDirection){ 258 | switch (direction) { 259 | case CardinalDirection.west: 260 | let topLeftPointWest = google.maps.geometry.spherical.computeOffset(origin, (solarPanelWidth * 0.3048), 270); 261 | let bottomRightPointWest = google.maps.geometry.spherical.computeOffset(origin, (solarPanelHeight * 0.3048), 180); 262 | let bottomLeftPointWest = google.maps.geometry.spherical.computeOffset(bottomRightPointWest, (solarPanelWidth * 0.3048), 270); 263 | let solarPanelVerteciesWest = [origin, topLeftPointWest, bottomLeftPointWest, bottomRightPointWest]; 264 | 265 | let fullyWithinRoofPanelWest = true; 266 | let anyPointWithinRoofPanelWest = false; 267 | solarPanelVerteciesWest.map((vertex) => { 268 | if(!google.maps.geometry.poly.containsLocation(vertex, this.panel)){ 269 | fullyWithinRoofPanelWest = false; 270 | }else{ 271 | anyPointWithinRoofPanelWest = true; 272 | } 273 | }); 274 | 275 | if(fullyWithinRoofPanelWest){ 276 | let newSolarPanel = new google.maps.Polygon({ 277 | paths: solarPanelVerteciesWest, 278 | strokeColor: "#00FF00", 279 | strokeOpacity: 0.8, 280 | strokeWeight: 2, 281 | fillColor: "#00FF00", 282 | fillOpacity: 0.35, 283 | }); 284 | newSolarPanel.addListener('click', () => { 285 | this.delete(); 286 | }); 287 | newSolarPanel.addListener('mouseover', () => {setPanelHovering(this.index);}); 288 | newSolarPanel.addListener('mouseout', () => {setPanelHovering(undefined);}); 289 | this.solarPanels.push(newSolarPanel); 290 | this.solarPanels[this.solarPanels.length-1].setMap(mapRef.current!); 291 | } 292 | 293 | if(anyPointWithinRoofPanelWest){ 294 | this.drawRowOfPanels(topLeftPointWest, CardinalDirection.west); 295 | } 296 | break; 297 | case CardinalDirection.east: 298 | let topRightPointEast = google.maps.geometry.spherical.computeOffset(origin, (solarPanelWidth * 0.3048), 90); 299 | let bottomLeftPointEast = google.maps.geometry.spherical.computeOffset(origin, (solarPanelHeight * 0.3048), 180); 300 | let bottomRightPointEast = google.maps.geometry.spherical.computeOffset(bottomLeftPointEast, (solarPanelWidth * 0.3048), 90); 301 | let solarPanelVerteciesEast = [origin, topRightPointEast, bottomRightPointEast, bottomLeftPointEast]; 302 | 303 | let fullyWithinRoofPanelEast = true; 304 | let anyPointWithinRoofPanelEast = false; 305 | solarPanelVerteciesEast.map((vertex) => { 306 | if(!google.maps.geometry.poly.containsLocation(vertex, this.panel)){ 307 | fullyWithinRoofPanelEast = false; 308 | }else{ 309 | anyPointWithinRoofPanelEast = true; 310 | } 311 | }); 312 | 313 | if(fullyWithinRoofPanelEast){ 314 | let newSolarPanel = new google.maps.Polygon({ 315 | paths: solarPanelVerteciesEast, 316 | strokeColor: "#00FF00", 317 | strokeOpacity: 0.8, 318 | strokeWeight: 2, 319 | fillColor: "#00FF00", 320 | fillOpacity: 0.35, 321 | }); 322 | newSolarPanel.addListener('click', () => { 323 | this.delete(); 324 | }); 325 | newSolarPanel.addListener('mouseover', () => {setPanelHovering(this.index);}); 326 | newSolarPanel.addListener('mouseout', () => {setPanelHovering(undefined);}); 327 | this.solarPanels.push(newSolarPanel); 328 | this.solarPanels[this.solarPanels.length-1].setMap(mapRef.current!); 329 | } 330 | 331 | if(anyPointWithinRoofPanelEast){ 332 | this.drawRowOfPanels(topRightPointEast, CardinalDirection.east); 333 | } 334 | break; 335 | default: 336 | break; 337 | } 338 | } 339 | 340 | 341 | delete(){ 342 | this.isDeleted = true; 343 | this.panel.setMap(null); 344 | this.solarPanels.map((panel) => { 345 | panel.setMap(null); 346 | }); 347 | setDeletedPanels(deletedPanels => [...deletedPanels, this.index]); 348 | } 349 | 350 | addBack(){ 351 | this.isDeleted = false; 352 | this.panel.setMap(mapRef.current!); 353 | this.solarPanels.map((panel) => { 354 | panel.setMap(mapRef.current!); 355 | }); 356 | setDeletedPanels(deletedPanels => deletedPanels.filter(item => item !== this.index)); 357 | } 358 | }; 359 | 360 | let addRoofSegmment = (points: LatLngLiteral[]) => { 361 | let index = roofPanels.length; 362 | setRoofPanels([...roofPanels, new roofPanel(points, index)]); 363 | }; 364 | 365 | let getRoofArea = () => { 366 | let area = 0; 367 | roofPanels.forEach((panel) => { 368 | if(!panel.isDeleted){ 369 | area += panel.area; 370 | } 371 | }); 372 | 373 | return area; 374 | } 375 | 376 | 377 | return( 378 |
379 |
380 |

Solar Roof Measurement Calculator

381 |

Figure out how many solar panels you can fit on your roof.

382 | { 384 | setHome(position); 385 | mapRef.current?.panTo(position); 386 | }} 387 | /> 388 |
389 |

Drawn Panels

390 |

Draw polygons over all south, east and west facing sections of your roof.

391 | {deletedPanels.length > 0 &&
392 |

Click to add panel back:

393 |
394 | {deletedPanels && deletedPanels.map((panelIndex) => { 395 | return 398 | })} 399 |
400 |
} 401 | {deletedPanels && roofPanels.length>0 && roofPanels.map((panel, index) => { 402 | if(!panel.isDeleted){ 403 | let area = panel.area; 404 | return ( 405 |
406 |

Panel {index+1}

407 |

Area: {area.toFixed(2)} ft²

408 | {panel.points.length === 3 &&

Panels: {panel.solarPanels.length} solar panels

} 409 | 417 |
418 | ); 419 | }else{ 420 | return (<>); 421 | } 422 | })} 423 |
424 |
425 |

Summary

426 |

Total Area: {getRoofArea().toFixed(2)} ft²

427 |

Ender your home's average monthly energy consumption below:

428 |
432 | { 435 | setEnergyConsumption(Number(e.target.value)) 436 | }} 437 | className="combobox-input" 438 | placeholder="Energy consumption" 439 | style={{ 440 | 'width': '25%', 441 | 'minWidth': '50px', 442 | 'margin': '5px 10px 5px 0', 443 | 'borderRadius': '2px', 444 | 'textAlign': 'center' 445 | }} 446 | /> 447 |

kWh per month

448 |
449 |

450 | You can offset {(getRoofArea()/solarPanelArea*solarPanelEnergyOutput/energyConsumption*100).toFixed(0)}% of 451 | your home's energy using {(getRoofArea()/solarPanelArea).toFixed(0)} solar panels solar panels which 452 | will generate {(getRoofArea()/solarPanelArea*solarPanelEnergyOutput).toFixed(2)} kWh of energy 453 | per month. 454 |

455 |
456 |
457 |
458 | {panelHovering !== undefined &&
Currently hovering over: Panel {panelHovering+1}
} 459 | { 466 | addBoxPoint(e.latLng?.toJSON()!); 467 | }} 468 | > 469 | {boxPoints.length > 0 && boxPoints.map((coordinates, index) => ( 470 | { 475 | if(index === 0 && boxPoints.length >= 3){ 476 | addRoofSegmment(boxPoints); 477 | setBoxPoints([]); 478 | } 479 | }} 480 | /> 481 | ))} 482 | {home && } 483 | 484 |
485 |
486 | ); 487 | } --------------------------------------------------------------------------------